Skip to content

Commit 0269227

Browse files
committed
Factor out unparsing machinery
1 parent fc3f78b commit 0269227

File tree

5 files changed

+123
-74
lines changed

5 files changed

+123
-74
lines changed

src/language/cli/output.ts

Lines changed: 31 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
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'
76

87
export const handleOutput = async (
98
process: NodeJS.Process,
@@ -16,91 +15,52 @@ export const handleOutput = async (
1615
'output-format': { type: 'string' },
1716
},
1817
})
19-
if (args.values['output-format'] === undefined) {
18+
const outputFormat = args.values['output-format']
19+
if (outputFormat === undefined) {
2020
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-
)
2521
} else {
22+
let notation: Notation
23+
if (outputFormat === 'json') {
24+
notation = prettyJson
25+
} else {
26+
throw new Error(`Unsupported output format: "${outputFormat}"`)
27+
}
28+
2629
const result = await command()
2730
return either.match(result, {
2831
left: error => {
2932
throw new Error(error.message) // TODO: Improve error reporting.
3033
},
3134
right: output => {
32-
writeJSON(process.stdout, output)
35+
writeOutput(process.stdout, notation, output)
3336
return undefined
3437
},
3538
})
3639
}
3740
}
3841

39-
export const writeJSON = (
42+
export const writeOutput = (
4043
writeStream: NodeJS.WritableStream,
44+
notation: Notation,
4145
output: SyntaxTree,
4246
): 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
47+
const outputAsString = unparse(output, notation)
48+
if (either.isLeft(outputAsString)) {
49+
throw new Error(outputAsString.value.message)
8750
} 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')
51+
writeStream.write(outputAsString.value)
9952

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

src/language/runtime/keywords.ts

Lines changed: 4 additions & 3 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 {
@@ -17,6 +17,7 @@ import {
1717
lookupPropertyOfObjectNode,
1818
makeUnelaboratedObjectNode,
1919
} from '../semantics/object-node.js'
20+
import { prettyJson } from '../unparsing/pretty-json.js'
2021

2122
const unserializableFunction = () =>
2223
either.makeLeft({
@@ -117,8 +118,8 @@ const runtimeContext = makeObjectNode({
117118
message: serializationResult.value.message,
118119
})
119120
} else {
120-
writeJSON(process.stderr, serializationResult.value)
121-
return either.makeRight(makeObjectNode({}))
121+
writeOutput(process.stderr, prettyJson, serializationResult.value)
122+
return either.makeRight(output)
122123
}
123124
},
124125
),

src/language/unparsing.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { Either } from '../adts.js'
2+
import type { UnserializableValueError } from './errors.js'
3+
import type { Atom, Molecule } from './parsing.js'
4+
import type { Notation } from './unparsing/unparsing-utilities.js'
5+
6+
export { type Notation } from './unparsing/unparsing-utilities.js'
7+
8+
export const unparse = (
9+
value: Atom | Molecule,
10+
notation: Notation,
11+
): Either<UnserializableValueError, string> =>
12+
typeof value === 'object'
13+
? notation.molecule(value, notation)
14+
: notation.atom(value)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import kleur from 'kleur'
2+
import { either } from '../../adts.js'
3+
import type { Right } from '../../adts/either.js'
4+
import type { Atom, Molecule } from '../parsing.js'
5+
import { indent, type Notation } from './unparsing-utilities.js'
6+
7+
const quote = kleur.dim('"')
8+
const colon = kleur.dim(':')
9+
const comma = kleur.dim(',')
10+
const openBrace = kleur.dim('{')
11+
const closeBrace = kleur.dim('}')
12+
13+
const escapeStringContents = (value: string) =>
14+
value.replace('\\', '\\\\').replace('"', '\\"')
15+
16+
const key = (value: Atom): string =>
17+
quote.concat(kleur.bold(escapeStringContents(value))).concat(quote)
18+
19+
const atom = (value: Atom): Right<string> =>
20+
either.makeRight(
21+
quote.concat(
22+
escapeStringContents(
23+
/^@[^@]/.test(value) ? kleur.bold(kleur.underline(value)) : value,
24+
),
25+
quote,
26+
),
27+
)
28+
29+
const molecule = (value: Molecule): Right<string> => {
30+
const entries = Object.entries(value)
31+
if (entries.length === 0) {
32+
return either.makeRight(openBrace.concat(closeBrace))
33+
} else {
34+
const keyValuePairs: string = Object.entries(value)
35+
.map(([propertyKey, propertyValue]) =>
36+
key(propertyKey)
37+
.concat(colon)
38+
.concat(' ')
39+
.concat(atomOrMolecule(propertyValue).value),
40+
)
41+
.join(comma.concat('\n'))
42+
43+
return either.makeRight(
44+
openBrace
45+
.concat('\n')
46+
.concat(indent(2, keyValuePairs))
47+
.concat('\n')
48+
.concat(closeBrace),
49+
)
50+
}
51+
}
52+
53+
const atomOrMolecule = (value: Atom | Molecule) =>
54+
typeof value === 'string' ? atom(value) : molecule(value)
55+
56+
export const prettyJson: Notation = { atom, molecule }
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type Either } from '../../adts.js'
2+
import type { UnserializableValueError } from '../errors.js'
3+
import type { Atom, Molecule } from '../parsing.js'
4+
5+
export type Notation = {
6+
readonly atom: (value: Atom) => Either<UnserializableValueError, string>
7+
readonly molecule: (
8+
value: Molecule,
9+
notation: Notation,
10+
) => Either<UnserializableValueError, string>
11+
}
12+
13+
export const indent = (spaces: number, textToIndent: string) => {
14+
const indentation = ' '.repeat(spaces)
15+
return indentation
16+
.concat(textToIndent)
17+
.replace(/(\r?\n)/g, `$1${indentation}`)
18+
}

0 commit comments

Comments
 (0)