Skip to content

Commit bb78ac9

Browse files
authored
Merge pull request #9 from mkantor/plz-output-notation
Support `--output-format=plz`
2 parents e3212cd + a6018fc commit bb78ac9

File tree

15 files changed

+340
-101
lines changed

15 files changed

+340
-101
lines changed

src/language/cli/output.ts

Lines changed: 34 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import kleur from 'kleur'
21
import { parseArgs } from 'util'
32
import { either, type Either } from '../../adts.js'
4-
import { withPhantomData } from '../../phantom-data.js'
5-
import { type Molecule } from '../parsing.js'
6-
import { type Canonicalized, type SyntaxTree } from '../parsing/syntax-tree.js'
3+
import { type SyntaxTree } from '../parsing/syntax-tree.js'
4+
import { unparse, type Notation } from '../unparsing.js'
5+
import { prettyJson } from '../unparsing/pretty-json.js'
6+
import { prettyPlz } from '../unparsing/pretty-plz.js'
77

88
export const handleOutput = async (
99
process: NodeJS.Process,
@@ -16,91 +16,54 @@ export const handleOutput = async (
1616
'output-format': { type: 'string' },
1717
},
1818
})
19-
if (args.values['output-format'] === undefined) {
19+
const outputFormat = args.values['output-format']
20+
if (outputFormat === undefined) {
2021
throw new Error('Missing required option: --output-format')
21-
} else if (args.values['output-format'] !== 'json') {
22-
throw new Error(
23-
`Unsupported output format: "${args.values['output-format']}"`,
24-
)
2522
} else {
23+
let notation: Notation
24+
if (outputFormat === 'json') {
25+
notation = prettyJson
26+
} else if (outputFormat === 'plz') {
27+
notation = prettyPlz
28+
} else {
29+
throw new Error(`Unsupported output format: "${outputFormat}"`)
30+
}
31+
2632
const result = await command()
2733
return either.match(result, {
2834
left: error => {
2935
throw new Error(error.message) // TODO: Improve error reporting.
3036
},
3137
right: output => {
32-
writeJSON(process.stdout, output)
38+
writeOutput(process.stdout, notation, output)
3339
return undefined
3440
},
3541
})
3642
}
3743
}
3844

39-
export const writeJSON = (
45+
export const writeOutput = (
4046
writeStream: NodeJS.WritableStream,
47+
notation: Notation,
4148
output: SyntaxTree,
4249
): void => {
43-
writeStream.write(stringifyAsPrettyJSON(output))
44-
45-
// Writing a newline ensures that output is flushed before terminating, otherwise nothing may be
46-
// printed to the console. See:
47-
// - <https://github.com/nodejs/node/issues/6379>
48-
// - <https://github.com/nodejs/node/issues/6456>
49-
// - <https://github.com/nodejs/node/issues/2972>
50-
// - …and many other near-duplicate issues
51-
//
52-
// I've tried other workarounds such as explicitly terminating via `process.exit`, passing a
53-
// callback to `writeStream.write` (ensuring the returned `Promise` is not resolved until it is
54-
// called), and explicitly calling `writeStream.end`/`writeStream.uncork` and so far this is the
55-
// only workaround which reliably results in the desired behavior.
56-
writeStream.write('\n')
57-
}
58-
59-
const indent = (spaces: number, textToIndent: string) => {
60-
const indentation = ' '.repeat(spaces)
61-
return (indentation + textToIndent).replace(/(\r?\n)/g, `$1${indentation}`)
62-
}
63-
64-
const quote = kleur.dim('"')
65-
const colon = kleur.dim(':')
66-
const comma = kleur.dim(',')
67-
const openBrace = kleur.dim('{')
68-
const closeBrace = kleur.dim('}')
69-
70-
const escapeStringContents = (value: string) =>
71-
value.replace('\\', '\\\\').replace('"', '\\"')
72-
73-
const key = (value: string): string =>
74-
quote + kleur.bold(escapeStringContents(value)) + quote
75-
76-
const string = (value: string): string =>
77-
quote +
78-
escapeStringContents(
79-
/^@[^@]/.test(value) ? kleur.bold(kleur.underline(value)) : value,
80-
) +
81-
quote
82-
83-
const object = (value: Molecule): string => {
84-
const entries = Object.entries(value)
85-
if (entries.length === 0) {
86-
return openBrace + closeBrace
50+
const outputAsString = unparse(output, notation)
51+
if (either.isLeft(outputAsString)) {
52+
throw new Error(outputAsString.value.message)
8753
} else {
88-
const keyValuePairs: string = Object.entries(value)
89-
.map(
90-
([propertyKey, propertyValue]) =>
91-
key(propertyKey) +
92-
colon +
93-
' ' +
94-
stringifyAsPrettyJSON(
95-
withPhantomData<Canonicalized>()(propertyValue),
96-
),
97-
)
98-
.join(comma + '\n')
54+
writeStream.write(outputAsString.value)
9955

100-
return openBrace + '\n' + indent(2, keyValuePairs) + '\n' + closeBrace
56+
// Writing a newline ensures that output is flushed before terminating, otherwise nothing may be
57+
// printed to the console. See:
58+
// - <https://github.com/nodejs/node/issues/6379>
59+
// - <https://github.com/nodejs/node/issues/6456>
60+
// - <https://github.com/nodejs/node/issues/2972>
61+
// - …and many other near-duplicate issues
62+
//
63+
// I've tried other workarounds such as explicitly terminating via `process.exit`, passing a
64+
// callback to `writeStream.write` (ensuring the returned `Promise` is not resolved until it is
65+
// called), and explicitly calling `writeStream.end`/`writeStream.uncork` and so far this is the
66+
// only workaround which reliably results in the desired behavior.
67+
writeStream.write('\n')
10168
}
10269
}
103-
104-
const stringifyAsPrettyJSON = (output: SyntaxTree) => {
105-
return typeof output === 'string' ? string(output) : object(output)
106-
}

src/language/runtime/keywords.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { parseArgs } from 'util'
22
import { either, option, type Option } from '../../adts.js'
3-
import { writeJSON } from '../cli/output.js'
3+
import { writeOutput } from '../cli/output.js'
44
import { keywordHandlers as compilerKeywordHandlers } from '../compiling.js'
55
import type { Atom } from '../parsing.js'
66
import {
@@ -13,7 +13,11 @@ import {
1313
type KeywordHandlers,
1414
type SemanticGraph,
1515
} from '../semantics.js'
16-
import { lookupPropertyOfObjectNode } from '../semantics/object-node.js'
16+
import {
17+
lookupPropertyOfObjectNode,
18+
makeUnelaboratedObjectNode,
19+
} from '../semantics/object-node.js'
20+
import { prettyJson } from '../unparsing/pretty-json.js'
1721

1822
const unserializableFunction = () =>
1923
either.makeLeft({
@@ -114,8 +118,8 @@ const runtimeContext = makeObjectNode({
114118
message: serializationResult.value.message,
115119
})
116120
} else {
117-
writeJSON(process.stderr, serializationResult.value)
118-
return either.makeRight(makeObjectNode({}))
121+
writeOutput(process.stderr, prettyJson, serializationResult.value)
122+
return either.makeRight(output)
119123
}
120124
},
121125
),
@@ -164,7 +168,10 @@ const lookupWithinExpression = (
164168
expression: Expression,
165169
): Option<SemanticGraph> => {
166170
for (const key of keyAliases) {
167-
const result = lookupPropertyOfObjectNode(key, expression)
171+
const result = lookupPropertyOfObjectNode(
172+
key,
173+
makeUnelaboratedObjectNode(expression),
174+
)
168175
if (!option.isNone(result)) {
169176
return result
170177
}

src/language/semantics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export { prelude } from './semantics/prelude.js'
6060
export {
6161
applyKeyPathToSemanticGraph,
6262
containsAnyUnelaboratedNodes,
63+
isSemanticGraph,
6364
serialize,
6465
stringifySemanticGraphForEndUser,
6566
updateValueAtKeyPathInSemanticGraph,
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import type { ObjectNode, SemanticGraph } from '../semantics.js'
1+
import type { Molecule } from '../parsing.js'
2+
import type { SemanticGraph } from './semantic-graph.js'
23

3-
export type Expression = ObjectNode & {
4+
export type Expression = {
45
readonly 0: `@${string}`
56
}
67

7-
export const isExpression = (node: SemanticGraph): node is Expression =>
8+
export const isExpression = (
9+
node: SemanticGraph | Molecule,
10+
): node is Expression =>
811
typeof node === 'object' && typeof node[0] === 'string' && node[0][0] === '@'

src/language/semantics/expressions/apply-expression.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { either, type Either } from '../../../adts.js'
22
import type { ElaborationError } from '../../errors.js'
33
import type { Molecule } from '../../parsing.js'
4-
import { isExpression, type Expression } from '../expression.js'
5-
import { makeUnelaboratedObjectNode } from '../object-node.js'
4+
import { isExpression } from '../expression.js'
5+
import { makeUnelaboratedObjectNode, type ObjectNode } from '../object-node.js'
66
import { type SemanticGraph, type unelaboratedKey } from '../semantic-graph.js'
77
import { readArgumentsFromExpression } from './expression-utilities.js'
88

9-
export type ApplyExpression = Expression & {
9+
export type ApplyExpression = ObjectNode & {
1010
readonly 0: '@apply'
1111
readonly function: SemanticGraph | Molecule
1212
readonly argument: SemanticGraph | Molecule
1313
}
1414

1515
export const readApplyExpression = (
16-
node: SemanticGraph,
16+
node: SemanticGraph | Molecule,
1717
): Either<ElaborationError, ApplyExpression> =>
1818
isExpression(node)
1919
? either.map(

src/language/semantics/expressions/check-expression.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { either, type Either } from '../../../adts.js'
22
import type { ElaborationError } from '../../errors.js'
33
import type { Molecule } from '../../parsing.js'
4-
import { isExpression, type Expression } from '../expression.js'
5-
import { makeUnelaboratedObjectNode } from '../object-node.js'
4+
import { isExpression } from '../expression.js'
5+
import { makeUnelaboratedObjectNode, type ObjectNode } from '../object-node.js'
66
import { type SemanticGraph, type unelaboratedKey } from '../semantic-graph.js'
77
import { readArgumentsFromExpression } from './expression-utilities.js'
88

9-
export type CheckExpression = Expression & {
9+
export type CheckExpression = ObjectNode & {
1010
readonly 0: '@check'
1111
readonly value: SemanticGraph | Molecule
1212
readonly type: SemanticGraph | Molecule
1313
}
1414

1515
export const readCheckExpression = (
16-
node: SemanticGraph,
16+
node: SemanticGraph | Molecule,
1717
): Either<ElaborationError, CheckExpression> =>
1818
isExpression(node)
1919
? either.map(

src/language/semantics/expressions/expression-utilities.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ const lookupWithinExpression = (
7272
expression: Expression,
7373
): Option<SemanticGraph> => {
7474
for (const key of keyAliases) {
75-
const result = lookupPropertyOfObjectNode(key, expression)
75+
const result = lookupPropertyOfObjectNode(
76+
key,
77+
makeUnelaboratedObjectNode(expression),
78+
)
7679
if (!option.isNone(result)) {
7780
return result
7881
}

src/language/semantics/expressions/function-expression.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { either, type Either } from '../../../adts.js'
22
import type { ElaborationError } from '../../errors.js'
33
import type { Atom, Molecule } from '../../parsing.js'
4-
import { isExpression, type Expression } from '../expression.js'
5-
import { makeUnelaboratedObjectNode } from '../object-node.js'
4+
import { isExpression } from '../expression.js'
5+
import { makeUnelaboratedObjectNode, type ObjectNode } from '../object-node.js'
66
import {
77
serialize,
88
type SemanticGraph,
@@ -13,14 +13,14 @@ import {
1313
readArgumentsFromExpression,
1414
} from './expression-utilities.js'
1515

16-
export type FunctionExpression = Expression & {
16+
export type FunctionExpression = ObjectNode & {
1717
readonly 0: '@function'
1818
readonly parameter: Atom
1919
readonly body: SemanticGraph | Molecule
2020
}
2121

2222
export const readFunctionExpression = (
23-
node: SemanticGraph,
23+
node: SemanticGraph | Molecule,
2424
): Either<ElaborationError, FunctionExpression> =>
2525
isExpression(node)
2626
? either.flatMap(

src/language/semantics/expressions/lookup-expression.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { either, type Either } from '../../../adts.js'
22
import type { ElaborationError, InvalidExpressionError } from '../../errors.js'
3-
import { isExpression, type Expression } from '../expression.js'
3+
import type { Molecule } from '../../parsing.js'
4+
import { isExpression } from '../expression.js'
45
import { isFunctionNode } from '../function-node.js'
56
import { keyPathToMolecule, type KeyPath } from '../key-path.js'
67
import {
@@ -14,13 +15,13 @@ import {
1415
readArgumentsFromExpression,
1516
} from './expression-utilities.js'
1617

17-
export type LookupExpression = Expression & {
18+
export type LookupExpression = ObjectNode & {
1819
readonly 0: '@lookup'
1920
readonly query: ObjectNode
2021
}
2122

2223
export const readLookupExpression = (
23-
node: SemanticGraph,
24+
node: SemanticGraph | Molecule,
2425
): Either<ElaborationError, LookupExpression> =>
2526
isExpression(node)
2627
? either.flatMap(

src/language/semantics/expressions/runtime-expression.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { either, type Either } from '../../../adts.js'
22
import type { ElaborationError } from '../../errors.js'
3-
import { isExpression, type Expression } from '../expression.js'
3+
import type { Molecule } from '../../parsing.js'
4+
import { isExpression } from '../expression.js'
45
import { isFunctionNode } from '../function-node.js'
5-
import { makeUnelaboratedObjectNode } from '../object-node.js'
6+
import { makeUnelaboratedObjectNode, type ObjectNode } from '../object-node.js'
67
import {
78
containsAnyUnelaboratedNodes,
89
type SemanticGraph,
@@ -13,13 +14,13 @@ import {
1314
readArgumentsFromExpression,
1415
} from './expression-utilities.js'
1516

16-
export type RuntimeExpression = Expression & {
17+
export type RuntimeExpression = ObjectNode & {
1718
readonly 0: '@runtime'
1819
readonly function: SemanticGraph
1920
}
2021

2122
export const readRuntimeExpression = (
22-
node: SemanticGraph,
23+
node: SemanticGraph | Molecule,
2324
): Either<ElaborationError, RuntimeExpression> =>
2425
isExpression(node)
2526
? either.flatMap(

0 commit comments

Comments
 (0)