diff --git a/src/language/semantics/key-path.ts b/src/language/semantics/key-path.ts index f2d9cb9..d35109f 100644 --- a/src/language/semantics/key-path.ts +++ b/src/language/semantics/key-path.ts @@ -1,19 +1,15 @@ import { either } from '../../adts.js' import type { Atom, Molecule } from '../parsing.js' import { unparse } from '../unparsing.js' -import { prettyPlz } from '../unparsing/pretty-plz.js' +import { inlinePlz } from '../unparsing/inline-plz.js' export type KeyPath = readonly Atom[] export const stringifyKeyPathForEndUser = (keyPath: KeyPath): string => - either.match( - // TODO: Use single-line plz notation. - unparse(keyPathToMolecule(keyPath), prettyPlz), - { - right: stringifiedOutput => stringifiedOutput, - left: error => `(unserializable key path: ${error.message})`, - }, - ) + either.match(unparse(keyPathToMolecule(keyPath), inlinePlz), { + right: stringifiedOutput => stringifiedOutput, + left: error => `(unserializable key path: ${error.message})`, + }) export const keyPathToMolecule = (keyPath: KeyPath): Molecule => Object.fromEntries(keyPath.flatMap((key, index) => [[index, key]])) diff --git a/src/language/semantics/semantic-graph.ts b/src/language/semantics/semantic-graph.ts index 91ed07b..990ca89 100644 --- a/src/language/semantics/semantic-graph.ts +++ b/src/language/semantics/semantic-graph.ts @@ -7,7 +7,7 @@ import type { import type { Atom, Molecule } from '../parsing.js' import type { Canonicalized } from '../parsing/syntax-tree.js' import { unparse } from '../unparsing.js' -import { prettyPlz } from '../unparsing/pretty-plz.js' +import { inlinePlz } from '../unparsing/inline-plz.js' import { serializeFunctionNode, type FunctionNode } from './function-node.js' import { stringifyKeyPathForEndUser, type KeyPath } from './key-path.js' import { @@ -170,10 +170,7 @@ export const stringifySemanticGraphForEndUser = ( graph: SemanticGraph, ): string => either.match( - either.flatMap(serialize(graph), output => - // TODO: Use single-line plz notation. - unparse(output, prettyPlz), - ), + either.flatMap(serialize(graph), output => unparse(output, inlinePlz)), { right: stringifiedOutput => stringifiedOutput, left: error => `(unserializable value: ${error.message})`, diff --git a/src/language/unparsing/inline-plz.ts b/src/language/unparsing/inline-plz.ts new file mode 100644 index 0000000..4d936c5 --- /dev/null +++ b/src/language/unparsing/inline-plz.ts @@ -0,0 +1,179 @@ +import kleur from 'kleur' +import { either, type Either } from '../../adts.js' +import type { Right } from '../../adts/either.js' +import type { UnserializableValueError } from '../errors.js' +import type { Atom, Molecule } from '../parsing.js' +import { unquotedAtomParser } from '../parsing/atom.js' +import { + isSemanticGraph, + readApplyExpression, + readFunctionExpression, + readLookupExpression, + serialize, + type KeyPath, + type SemanticGraph, +} from '../semantics.js' +import { type Notation } from './unparsing-utilities.js' + +// TODO: Share implementation details with pretty plz notation. + +const dot = kleur.dim('.') +const quote = kleur.dim('"') +const colon = kleur.dim(':') +const comma = kleur.dim(',') +const openBrace = kleur.dim('{') +const closeBrace = kleur.dim('}') +const openParenthesis = kleur.dim('(') +const closeParenthesis = kleur.dim(')') +const arrow = kleur.dim('=>') + +const escapeStringContents = (value: string) => + value.replace('\\', '\\\\').replace('"', '\\"') + +const quoteIfNecessary = (value: string) => + !either.isLeft(unquotedAtomParser(value)) + ? value + : quote.concat(escapeStringContents(value)).concat(quote) + +const atom = (node: string): Right => + either.makeRight( + quoteIfNecessary( + /^@[^@]/.test(node) ? kleur.bold(kleur.underline(node)) : node, + ), + ) + +const molecule = ( + value: Molecule, +): Either => { + const functionExpressionResult = readFunctionExpression(value) + if (!either.isLeft(functionExpressionResult)) { + return sugaredFunction( + functionExpressionResult.value.parameter, + functionExpressionResult.value.body, + ) + } else { + const applyExpressionResult = readApplyExpression(value) + if (!either.isLeft(applyExpressionResult)) { + return sugaredApply( + applyExpressionResult.value.argument, + applyExpressionResult.value.function, + ) + } else { + const lookupExpressionResult = readLookupExpression(value) + if (!either.isLeft(lookupExpressionResult)) { + return sugaredLookup(lookupExpressionResult.value.query) + } else { + return sugarFreeMolecule(value) + } + } + } +} + +const sugaredLookup = (keyPathAsNode: Molecule | SemanticGraph) => { + const keyPath = Object.entries(keyPathAsNode).reduce( + (accumulator: KeyPath | 'invalid', [key, value]) => { + if (accumulator === 'invalid') { + return accumulator + } else { + if (key === String(accumulator.length) && typeof value === 'string') { + return [...accumulator, value] + } else { + return 'invalid' + } + } + }, + [], + ) + + if ( + keyPath !== 'invalid' && + Object.keys(keyPathAsNode).length === keyPath.length && + keyPath.every(key => !either.isLeft(unquotedAtomParser(key))) + ) { + return either.makeRight(kleur.cyan(colon.concat(keyPath.join(dot)))) + } else { + return either.flatMap(serializeIfNeeded(keyPathAsNode), serializedKeyPath => + either.map(atomOrMolecule(serializedKeyPath), keyPathAsString => + kleur.cyan(colon.concat(keyPathAsString)), + ), + ) + } +} + +const sugaredFunction = ( + parameterName: string, + body: Molecule | SemanticGraph, +) => + either.flatMap(serializeIfNeeded(body), serializedBody => + either.map(atomOrMolecule(serializedBody), bodyAsString => + [kleur.cyan(parameterName), arrow, bodyAsString].join(' '), + ), + ) + +const sugaredApply = ( + argument: Molecule | SemanticGraph, + functionToApply: Molecule | SemanticGraph, +) => + either.flatMap(serializeIfNeeded(functionToApply), serializedFunction => + either.flatMap( + atomOrMolecule(serializedFunction), + functionToApplyAsString => + either.flatMap(serializeIfNeeded(argument), serializedArgument => + either.map(atomOrMolecule(serializedArgument), argumentAsString => + functionToApplyAsString + .concat(openParenthesis) + .concat(argumentAsString) + .concat(closeParenthesis), + ), + ), + ), + ) + +const sugarFreeMolecule = (value: Molecule) => { + const entries = Object.entries(value) + if (entries.length === 0) { + return either.makeRight(openBrace + closeBrace) + } else { + const keyValuePairsAsStrings: string[] = [] + let ordinalPropertyKeyCounter = 0n + for (const [propertyKey, propertyValue] of entries) { + const valueAsStringResult = atomOrMolecule(propertyValue) + if (either.isLeft(valueAsStringResult)) { + return valueAsStringResult + } + + // Omit ordinal property keys: + if (propertyKey === String(ordinalPropertyKeyCounter)) { + keyValuePairsAsStrings.push(valueAsStringResult.value) + ordinalPropertyKeyCounter += 1n + } else { + keyValuePairsAsStrings.push( + kleur + .cyan(quoteIfNecessary(propertyKey).concat(colon)) + .concat(' ') + .concat(valueAsStringResult.value), + ) + } + } + + return either.makeRight( + openBrace + .concat(' ') + .concat(keyValuePairsAsStrings.join(comma.concat(' '))) + .concat(' ') + .concat(closeBrace), + ) + } +} + +const serializeIfNeeded = ( + nodeOrMolecule: SemanticGraph | Molecule, +): Either => + isSemanticGraph(nodeOrMolecule) + ? serialize(nodeOrMolecule) + : either.makeRight(nodeOrMolecule) + +const atomOrMolecule = (value: Atom | Molecule) => + typeof value === 'string' ? atom(value) : molecule(value) + +export const inlinePlz: Notation = { atom, molecule } diff --git a/src/language/unparsing/pretty-plz.ts b/src/language/unparsing/pretty-plz.ts index 7e4cbad..ea961b9 100644 --- a/src/language/unparsing/pretty-plz.ts +++ b/src/language/unparsing/pretty-plz.ts @@ -15,6 +15,8 @@ import { } from '../semantics.js' import { indent, type Notation } from './unparsing-utilities.js' +// TODO: Share implementation details with inline plz notation. + const dot = kleur.dim('.') const quote = kleur.dim('"') const colon = kleur.dim(':')