From fc3f78b84ae44cfc54d3280cce04dddab900a3fb Mon Sep 17 00:00:00 2001 From: Matt Kantor Date: Mon, 30 Dec 2024 18:07:56 -0500 Subject: [PATCH 1/3] Generalize some expression utilities to accept molecules --- src/language/runtime/keywords.ts | 10 ++++++++-- src/language/semantics/expression.ts | 9 ++++++--- src/language/semantics/expressions/apply-expression.ts | 8 ++++---- src/language/semantics/expressions/check-expression.ts | 8 ++++---- .../semantics/expressions/expression-utilities.ts | 5 ++++- .../semantics/expressions/function-expression.ts | 8 ++++---- .../semantics/expressions/lookup-expression.ts | 7 ++++--- .../semantics/expressions/runtime-expression.ts | 9 +++++---- src/language/semantics/expressions/todo-expression.ts | 4 ++-- 9 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/language/runtime/keywords.ts b/src/language/runtime/keywords.ts index c56e99a..92c437b 100644 --- a/src/language/runtime/keywords.ts +++ b/src/language/runtime/keywords.ts @@ -13,7 +13,10 @@ import { type KeywordHandlers, type SemanticGraph, } from '../semantics.js' -import { lookupPropertyOfObjectNode } from '../semantics/object-node.js' +import { + lookupPropertyOfObjectNode, + makeUnelaboratedObjectNode, +} from '../semantics/object-node.js' const unserializableFunction = () => either.makeLeft({ @@ -164,7 +167,10 @@ const lookupWithinExpression = ( expression: Expression, ): Option => { for (const key of keyAliases) { - const result = lookupPropertyOfObjectNode(key, expression) + const result = lookupPropertyOfObjectNode( + key, + makeUnelaboratedObjectNode(expression), + ) if (!option.isNone(result)) { return result } diff --git a/src/language/semantics/expression.ts b/src/language/semantics/expression.ts index 3698123..56bbf9d 100644 --- a/src/language/semantics/expression.ts +++ b/src/language/semantics/expression.ts @@ -1,8 +1,11 @@ -import type { ObjectNode, SemanticGraph } from '../semantics.js' +import type { Molecule } from '../parsing.js' +import type { SemanticGraph } from './semantic-graph.js' -export type Expression = ObjectNode & { +export type Expression = { readonly 0: `@${string}` } -export const isExpression = (node: SemanticGraph): node is Expression => +export const isExpression = ( + node: SemanticGraph | Molecule, +): node is Expression => typeof node === 'object' && typeof node[0] === 'string' && node[0][0] === '@' diff --git a/src/language/semantics/expressions/apply-expression.ts b/src/language/semantics/expressions/apply-expression.ts index 371a8cc..b7acecd 100644 --- a/src/language/semantics/expressions/apply-expression.ts +++ b/src/language/semantics/expressions/apply-expression.ts @@ -1,19 +1,19 @@ import { either, type Either } from '../../../adts.js' import type { ElaborationError } from '../../errors.js' import type { Molecule } from '../../parsing.js' -import { isExpression, type Expression } from '../expression.js' -import { makeUnelaboratedObjectNode } from '../object-node.js' +import { isExpression } from '../expression.js' +import { makeUnelaboratedObjectNode, type ObjectNode } from '../object-node.js' import { type SemanticGraph, type unelaboratedKey } from '../semantic-graph.js' import { readArgumentsFromExpression } from './expression-utilities.js' -export type ApplyExpression = Expression & { +export type ApplyExpression = ObjectNode & { readonly 0: '@apply' readonly function: SemanticGraph | Molecule readonly argument: SemanticGraph | Molecule } export const readApplyExpression = ( - node: SemanticGraph, + node: SemanticGraph | Molecule, ): Either => isExpression(node) ? either.map( diff --git a/src/language/semantics/expressions/check-expression.ts b/src/language/semantics/expressions/check-expression.ts index 351d3d7..d7d33f8 100644 --- a/src/language/semantics/expressions/check-expression.ts +++ b/src/language/semantics/expressions/check-expression.ts @@ -1,19 +1,19 @@ import { either, type Either } from '../../../adts.js' import type { ElaborationError } from '../../errors.js' import type { Molecule } from '../../parsing.js' -import { isExpression, type Expression } from '../expression.js' -import { makeUnelaboratedObjectNode } from '../object-node.js' +import { isExpression } from '../expression.js' +import { makeUnelaboratedObjectNode, type ObjectNode } from '../object-node.js' import { type SemanticGraph, type unelaboratedKey } from '../semantic-graph.js' import { readArgumentsFromExpression } from './expression-utilities.js' -export type CheckExpression = Expression & { +export type CheckExpression = ObjectNode & { readonly 0: '@check' readonly value: SemanticGraph | Molecule readonly type: SemanticGraph | Molecule } export const readCheckExpression = ( - node: SemanticGraph, + node: SemanticGraph | Molecule, ): Either => isExpression(node) ? either.map( diff --git a/src/language/semantics/expressions/expression-utilities.ts b/src/language/semantics/expressions/expression-utilities.ts index b3c543a..5ebb41b 100644 --- a/src/language/semantics/expressions/expression-utilities.ts +++ b/src/language/semantics/expressions/expression-utilities.ts @@ -72,7 +72,10 @@ const lookupWithinExpression = ( expression: Expression, ): Option => { for (const key of keyAliases) { - const result = lookupPropertyOfObjectNode(key, expression) + const result = lookupPropertyOfObjectNode( + key, + makeUnelaboratedObjectNode(expression), + ) if (!option.isNone(result)) { return result } diff --git a/src/language/semantics/expressions/function-expression.ts b/src/language/semantics/expressions/function-expression.ts index e37d5d3..bbf802e 100644 --- a/src/language/semantics/expressions/function-expression.ts +++ b/src/language/semantics/expressions/function-expression.ts @@ -1,8 +1,8 @@ import { either, type Either } from '../../../adts.js' import type { ElaborationError } from '../../errors.js' import type { Atom, Molecule } from '../../parsing.js' -import { isExpression, type Expression } from '../expression.js' -import { makeUnelaboratedObjectNode } from '../object-node.js' +import { isExpression } from '../expression.js' +import { makeUnelaboratedObjectNode, type ObjectNode } from '../object-node.js' import { serialize, type SemanticGraph, @@ -13,14 +13,14 @@ import { readArgumentsFromExpression, } from './expression-utilities.js' -export type FunctionExpression = Expression & { +export type FunctionExpression = ObjectNode & { readonly 0: '@function' readonly parameter: Atom readonly body: SemanticGraph | Molecule } export const readFunctionExpression = ( - node: SemanticGraph, + node: SemanticGraph | Molecule, ): Either => isExpression(node) ? either.flatMap( diff --git a/src/language/semantics/expressions/lookup-expression.ts b/src/language/semantics/expressions/lookup-expression.ts index 0271e26..9acf0b0 100644 --- a/src/language/semantics/expressions/lookup-expression.ts +++ b/src/language/semantics/expressions/lookup-expression.ts @@ -1,6 +1,7 @@ import { either, type Either } from '../../../adts.js' import type { ElaborationError, InvalidExpressionError } from '../../errors.js' -import { isExpression, type Expression } from '../expression.js' +import type { Molecule } from '../../parsing.js' +import { isExpression } from '../expression.js' import { isFunctionNode } from '../function-node.js' import { keyPathToMolecule, type KeyPath } from '../key-path.js' import { @@ -14,13 +15,13 @@ import { readArgumentsFromExpression, } from './expression-utilities.js' -export type LookupExpression = Expression & { +export type LookupExpression = ObjectNode & { readonly 0: '@lookup' readonly query: ObjectNode } export const readLookupExpression = ( - node: SemanticGraph, + node: SemanticGraph | Molecule, ): Either => isExpression(node) ? either.flatMap( diff --git a/src/language/semantics/expressions/runtime-expression.ts b/src/language/semantics/expressions/runtime-expression.ts index 1ccaa60..d1edffc 100644 --- a/src/language/semantics/expressions/runtime-expression.ts +++ b/src/language/semantics/expressions/runtime-expression.ts @@ -1,8 +1,9 @@ import { either, type Either } from '../../../adts.js' import type { ElaborationError } from '../../errors.js' -import { isExpression, type Expression } from '../expression.js' +import type { Molecule } from '../../parsing.js' +import { isExpression } from '../expression.js' import { isFunctionNode } from '../function-node.js' -import { makeUnelaboratedObjectNode } from '../object-node.js' +import { makeUnelaboratedObjectNode, type ObjectNode } from '../object-node.js' import { containsAnyUnelaboratedNodes, type SemanticGraph, @@ -13,13 +14,13 @@ import { readArgumentsFromExpression, } from './expression-utilities.js' -export type RuntimeExpression = Expression & { +export type RuntimeExpression = ObjectNode & { readonly 0: '@runtime' readonly function: SemanticGraph } export const readRuntimeExpression = ( - node: SemanticGraph, + node: SemanticGraph | Molecule, ): Either => isExpression(node) ? either.flatMap( diff --git a/src/language/semantics/expressions/todo-expression.ts b/src/language/semantics/expressions/todo-expression.ts index 111cb3c..3589722 100644 --- a/src/language/semantics/expressions/todo-expression.ts +++ b/src/language/semantics/expressions/todo-expression.ts @@ -1,5 +1,5 @@ -import { type Expression } from '../expression.js' +import type { ObjectNode } from '../object-node.js' -export type TodoExpression = Expression & { +export type TodoExpression = ObjectNode & { readonly 0: '@todo' } From 0269227a57ff896afa302db53668d9b3bcd85242 Mon Sep 17 00:00:00 2001 From: Matt Kantor Date: Wed, 1 Jan 2025 10:35:11 -0500 Subject: [PATCH 2/3] Factor out unparsing machinery --- src/language/cli/output.ts | 102 ++++++------------ src/language/runtime/keywords.ts | 7 +- src/language/unparsing.ts | 14 +++ src/language/unparsing/pretty-json.ts | 56 ++++++++++ src/language/unparsing/unparsing-utilities.ts | 18 ++++ 5 files changed, 123 insertions(+), 74 deletions(-) create mode 100644 src/language/unparsing.ts create mode 100644 src/language/unparsing/pretty-json.ts create mode 100644 src/language/unparsing/unparsing-utilities.ts diff --git a/src/language/cli/output.ts b/src/language/cli/output.ts index 979fdea..a885ea3 100644 --- a/src/language/cli/output.ts +++ b/src/language/cli/output.ts @@ -1,9 +1,8 @@ -import kleur from 'kleur' import { parseArgs } from 'util' import { either, type Either } from '../../adts.js' -import { withPhantomData } from '../../phantom-data.js' -import { type Molecule } from '../parsing.js' -import { type Canonicalized, type SyntaxTree } from '../parsing/syntax-tree.js' +import { type SyntaxTree } from '../parsing/syntax-tree.js' +import { unparse, type Notation } from '../unparsing.js' +import { prettyJson } from '../unparsing/pretty-json.js' export const handleOutput = async ( process: NodeJS.Process, @@ -16,91 +15,52 @@ export const handleOutput = async ( 'output-format': { type: 'string' }, }, }) - if (args.values['output-format'] === undefined) { + const outputFormat = args.values['output-format'] + if (outputFormat === undefined) { throw new Error('Missing required option: --output-format') - } else if (args.values['output-format'] !== 'json') { - throw new Error( - `Unsupported output format: "${args.values['output-format']}"`, - ) } else { + let notation: Notation + if (outputFormat === 'json') { + notation = prettyJson + } else { + throw new Error(`Unsupported output format: "${outputFormat}"`) + } + const result = await command() return either.match(result, { left: error => { throw new Error(error.message) // TODO: Improve error reporting. }, right: output => { - writeJSON(process.stdout, output) + writeOutput(process.stdout, notation, output) return undefined }, }) } } -export const writeJSON = ( +export const writeOutput = ( writeStream: NodeJS.WritableStream, + notation: Notation, output: SyntaxTree, ): void => { - writeStream.write(stringifyAsPrettyJSON(output)) - - // Writing a newline ensures that output is flushed before terminating, otherwise nothing may be - // printed to the console. See: - // - - // - - // - - // - …and many other near-duplicate issues - // - // I've tried other workarounds such as explicitly terminating via `process.exit`, passing a - // callback to `writeStream.write` (ensuring the returned `Promise` is not resolved until it is - // called), and explicitly calling `writeStream.end`/`writeStream.uncork` and so far this is the - // only workaround which reliably results in the desired behavior. - writeStream.write('\n') -} - -const indent = (spaces: number, textToIndent: string) => { - const indentation = ' '.repeat(spaces) - return (indentation + textToIndent).replace(/(\r?\n)/g, `$1${indentation}`) -} - -const quote = kleur.dim('"') -const colon = kleur.dim(':') -const comma = kleur.dim(',') -const openBrace = kleur.dim('{') -const closeBrace = kleur.dim('}') - -const escapeStringContents = (value: string) => - value.replace('\\', '\\\\').replace('"', '\\"') - -const key = (value: string): string => - quote + kleur.bold(escapeStringContents(value)) + quote - -const string = (value: string): string => - quote + - escapeStringContents( - /^@[^@]/.test(value) ? kleur.bold(kleur.underline(value)) : value, - ) + - quote - -const object = (value: Molecule): string => { - const entries = Object.entries(value) - if (entries.length === 0) { - return openBrace + closeBrace + const outputAsString = unparse(output, notation) + if (either.isLeft(outputAsString)) { + throw new Error(outputAsString.value.message) } else { - const keyValuePairs: string = Object.entries(value) - .map( - ([propertyKey, propertyValue]) => - key(propertyKey) + - colon + - ' ' + - stringifyAsPrettyJSON( - withPhantomData()(propertyValue), - ), - ) - .join(comma + '\n') + writeStream.write(outputAsString.value) - return openBrace + '\n' + indent(2, keyValuePairs) + '\n' + closeBrace + // Writing a newline ensures that output is flushed before terminating, otherwise nothing may be + // printed to the console. See: + // - + // - + // - + // - …and many other near-duplicate issues + // + // I've tried other workarounds such as explicitly terminating via `process.exit`, passing a + // callback to `writeStream.write` (ensuring the returned `Promise` is not resolved until it is + // called), and explicitly calling `writeStream.end`/`writeStream.uncork` and so far this is the + // only workaround which reliably results in the desired behavior. + writeStream.write('\n') } } - -const stringifyAsPrettyJSON = (output: SyntaxTree) => { - return typeof output === 'string' ? string(output) : object(output) -} diff --git a/src/language/runtime/keywords.ts b/src/language/runtime/keywords.ts index 92c437b..12b418c 100644 --- a/src/language/runtime/keywords.ts +++ b/src/language/runtime/keywords.ts @@ -1,6 +1,6 @@ import { parseArgs } from 'util' import { either, option, type Option } from '../../adts.js' -import { writeJSON } from '../cli/output.js' +import { writeOutput } from '../cli/output.js' import { keywordHandlers as compilerKeywordHandlers } from '../compiling.js' import type { Atom } from '../parsing.js' import { @@ -17,6 +17,7 @@ import { lookupPropertyOfObjectNode, makeUnelaboratedObjectNode, } from '../semantics/object-node.js' +import { prettyJson } from '../unparsing/pretty-json.js' const unserializableFunction = () => either.makeLeft({ @@ -117,8 +118,8 @@ const runtimeContext = makeObjectNode({ message: serializationResult.value.message, }) } else { - writeJSON(process.stderr, serializationResult.value) - return either.makeRight(makeObjectNode({})) + writeOutput(process.stderr, prettyJson, serializationResult.value) + return either.makeRight(output) } }, ), diff --git a/src/language/unparsing.ts b/src/language/unparsing.ts new file mode 100644 index 0000000..5a31b51 --- /dev/null +++ b/src/language/unparsing.ts @@ -0,0 +1,14 @@ +import type { Either } from '../adts.js' +import type { UnserializableValueError } from './errors.js' +import type { Atom, Molecule } from './parsing.js' +import type { Notation } from './unparsing/unparsing-utilities.js' + +export { type Notation } from './unparsing/unparsing-utilities.js' + +export const unparse = ( + value: Atom | Molecule, + notation: Notation, +): Either => + typeof value === 'object' + ? notation.molecule(value, notation) + : notation.atom(value) diff --git a/src/language/unparsing/pretty-json.ts b/src/language/unparsing/pretty-json.ts new file mode 100644 index 0000000..61e6946 --- /dev/null +++ b/src/language/unparsing/pretty-json.ts @@ -0,0 +1,56 @@ +import kleur from 'kleur' +import { either } from '../../adts.js' +import type { Right } from '../../adts/either.js' +import type { Atom, Molecule } from '../parsing.js' +import { indent, type Notation } from './unparsing-utilities.js' + +const quote = kleur.dim('"') +const colon = kleur.dim(':') +const comma = kleur.dim(',') +const openBrace = kleur.dim('{') +const closeBrace = kleur.dim('}') + +const escapeStringContents = (value: string) => + value.replace('\\', '\\\\').replace('"', '\\"') + +const key = (value: Atom): string => + quote.concat(kleur.bold(escapeStringContents(value))).concat(quote) + +const atom = (value: Atom): Right => + either.makeRight( + quote.concat( + escapeStringContents( + /^@[^@]/.test(value) ? kleur.bold(kleur.underline(value)) : value, + ), + quote, + ), + ) + +const molecule = (value: Molecule): Right => { + const entries = Object.entries(value) + if (entries.length === 0) { + return either.makeRight(openBrace.concat(closeBrace)) + } else { + const keyValuePairs: string = Object.entries(value) + .map(([propertyKey, propertyValue]) => + key(propertyKey) + .concat(colon) + .concat(' ') + .concat(atomOrMolecule(propertyValue).value), + ) + .join(comma.concat('\n')) + + return either.makeRight( + openBrace + .concat('\n') + .concat(indent(2, keyValuePairs)) + .concat('\n') + .concat(closeBrace), + ) + } +} + +const atomOrMolecule = (value: Atom | Molecule) => + typeof value === 'string' ? atom(value) : molecule(value) + +export const prettyJson: Notation = { atom, molecule } diff --git a/src/language/unparsing/unparsing-utilities.ts b/src/language/unparsing/unparsing-utilities.ts new file mode 100644 index 0000000..ba0cebe --- /dev/null +++ b/src/language/unparsing/unparsing-utilities.ts @@ -0,0 +1,18 @@ +import { type Either } from '../../adts.js' +import type { UnserializableValueError } from '../errors.js' +import type { Atom, Molecule } from '../parsing.js' + +export type Notation = { + readonly atom: (value: Atom) => Either + readonly molecule: ( + value: Molecule, + notation: Notation, + ) => Either +} + +export const indent = (spaces: number, textToIndent: string) => { + const indentation = ' '.repeat(spaces) + return indentation + .concat(textToIndent) + .replace(/(\r?\n)/g, `$1${indentation}`) +} From a6018fc042e8ee386f081741fddcce0d1ff2bc37 Mon Sep 17 00:00:00 2001 From: Matt Kantor Date: Wed, 1 Jan 2025 12:47:02 -0500 Subject: [PATCH 3/3] Support plz as an output format --- src/language/cli/output.ts | 3 + src/language/semantics.ts | 1 + src/language/unparsing/pretty-plz.ts | 172 +++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 src/language/unparsing/pretty-plz.ts diff --git a/src/language/cli/output.ts b/src/language/cli/output.ts index a885ea3..b4a9c16 100644 --- a/src/language/cli/output.ts +++ b/src/language/cli/output.ts @@ -3,6 +3,7 @@ import { either, type Either } from '../../adts.js' import { type SyntaxTree } from '../parsing/syntax-tree.js' import { unparse, type Notation } from '../unparsing.js' import { prettyJson } from '../unparsing/pretty-json.js' +import { prettyPlz } from '../unparsing/pretty-plz.js' export const handleOutput = async ( process: NodeJS.Process, @@ -22,6 +23,8 @@ export const handleOutput = async ( let notation: Notation if (outputFormat === 'json') { notation = prettyJson + } else if (outputFormat === 'plz') { + notation = prettyPlz } else { throw new Error(`Unsupported output format: "${outputFormat}"`) } diff --git a/src/language/semantics.ts b/src/language/semantics.ts index 4793e91..3eac406 100644 --- a/src/language/semantics.ts +++ b/src/language/semantics.ts @@ -60,6 +60,7 @@ export { prelude } from './semantics/prelude.js' export { applyKeyPathToSemanticGraph, containsAnyUnelaboratedNodes, + isSemanticGraph, serialize, stringifySemanticGraphForEndUser, updateValueAtKeyPathInSemanticGraph, diff --git a/src/language/unparsing/pretty-plz.ts b/src/language/unparsing/pretty-plz.ts new file mode 100644 index 0000000..efadb8c --- /dev/null +++ b/src/language/unparsing/pretty-plz.ts @@ -0,0 +1,172 @@ +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 { indent, type Notation } from './unparsing-utilities.js' + +const dot = kleur.dim('.') +const quote = kleur.dim('"') +const colon = 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 => typeof key === 'string' && !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[] = [] + for (const [propertyKey, propertyValue] of entries) { + const valueAsStringResult = atomOrMolecule(propertyValue) + if (either.isLeft(valueAsStringResult)) { + return valueAsStringResult + } + + keyValuePairsAsStrings.push( + // TODO: Intelligently omit all ordinal property keys, not just `0`. + (propertyKey === '0' + ? '' + : kleur.cyan(quoteIfNecessary(propertyKey).concat(colon)).concat(' ') + ).concat(valueAsStringResult.value), + ) + } + + return either.makeRight( + openBrace + .concat('\n') + .concat(indent(2, keyValuePairsAsStrings.join('\n'))) + .concat('\n') + .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 prettyPlz: Notation = { atom, molecule }