Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 34 additions & 71 deletions src/language/cli/output.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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'
import { prettyPlz } from '../unparsing/pretty-plz.js'

export const handleOutput = async (
process: NodeJS.Process,
Expand All @@ -16,91 +16,54 @@ 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 if (outputFormat === 'plz') {
notation = prettyPlz
} 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:
// - <https://github.com/nodejs/node/issues/6379>
// - <https://github.com/nodejs/node/issues/6456>
// - <https://github.com/nodejs/node/issues/2972>
// - …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<Canonicalized>()(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:
// - <https://github.com/nodejs/node/issues/6379>
// - <https://github.com/nodejs/node/issues/6456>
// - <https://github.com/nodejs/node/issues/2972>
// - …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)
}
17 changes: 12 additions & 5 deletions src/language/runtime/keywords.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -13,7 +13,11 @@ import {
type KeywordHandlers,
type SemanticGraph,
} from '../semantics.js'
import { lookupPropertyOfObjectNode } from '../semantics/object-node.js'
import {
lookupPropertyOfObjectNode,
makeUnelaboratedObjectNode,
} from '../semantics/object-node.js'
import { prettyJson } from '../unparsing/pretty-json.js'

const unserializableFunction = () =>
either.makeLeft({
Expand Down Expand Up @@ -114,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)
}
},
),
Expand Down Expand Up @@ -164,7 +168,10 @@ const lookupWithinExpression = (
expression: Expression,
): Option<SemanticGraph> => {
for (const key of keyAliases) {
const result = lookupPropertyOfObjectNode(key, expression)
const result = lookupPropertyOfObjectNode(
key,
makeUnelaboratedObjectNode(expression),
)
if (!option.isNone(result)) {
return result
}
Expand Down
1 change: 1 addition & 0 deletions src/language/semantics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export { prelude } from './semantics/prelude.js'
export {
applyKeyPathToSemanticGraph,
containsAnyUnelaboratedNodes,
isSemanticGraph,
serialize,
stringifySemanticGraphForEndUser,
updateValueAtKeyPathInSemanticGraph,
Expand Down
9 changes: 6 additions & 3 deletions src/language/semantics/expression.ts
Original file line number Diff line number Diff line change
@@ -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] === '@'
8 changes: 4 additions & 4 deletions src/language/semantics/expressions/apply-expression.ts
Original file line number Diff line number Diff line change
@@ -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<ElaborationError, ApplyExpression> =>
isExpression(node)
? either.map(
Expand Down
8 changes: 4 additions & 4 deletions src/language/semantics/expressions/check-expression.ts
Original file line number Diff line number Diff line change
@@ -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<ElaborationError, CheckExpression> =>
isExpression(node)
? either.map(
Expand Down
5 changes: 4 additions & 1 deletion src/language/semantics/expressions/expression-utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ const lookupWithinExpression = (
expression: Expression,
): Option<SemanticGraph> => {
for (const key of keyAliases) {
const result = lookupPropertyOfObjectNode(key, expression)
const result = lookupPropertyOfObjectNode(
key,
makeUnelaboratedObjectNode(expression),
)
if (!option.isNone(result)) {
return result
}
Expand Down
8 changes: 4 additions & 4 deletions src/language/semantics/expressions/function-expression.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<ElaborationError, FunctionExpression> =>
isExpression(node)
? either.flatMap(
Expand Down
7 changes: 4 additions & 3 deletions src/language/semantics/expressions/lookup-expression.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<ElaborationError, LookupExpression> =>
isExpression(node)
? either.flatMap(
Expand Down
9 changes: 5 additions & 4 deletions src/language/semantics/expressions/runtime-expression.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<ElaborationError, RuntimeExpression> =>
isExpression(node)
? either.flatMap(
Expand Down
4 changes: 2 additions & 2 deletions src/language/semantics/expressions/todo-expression.ts
Original file line number Diff line number Diff line change
@@ -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'
}
14 changes: 14 additions & 0 deletions src/language/unparsing.ts
Original file line number Diff line number Diff line change
@@ -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<UnserializableValueError, string> =>
typeof value === 'object'
? notation.molecule(value, notation)
: notation.atom(value)
Loading
Loading