From 188c9d62b74fd4802eb0c38533734c16b8032bad Mon Sep 17 00:00:00 2001 From: Reuben Thomas Date: Fri, 6 Sep 2024 23:49:29 +0100 Subject: [PATCH] Rewrite 'ursa fmt' using Topiary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Temporarily comment out the test test/bad-assignment, as the current tree-sitter grammar does not accept e.g. “2 := 3”. I think this is good! and will make the Ohm grammar refuse it as well in a follow-up commit. --- package.json | 6 +- src/topiary/ursa.scm | 128 ++++ src/ursa/cli.ts | 18 +- src/ursa/examples.test.ts | 2 +- src/ursa/fmt.ts | 663 +----------------- test/bad-call.reformatted-stderr | 16 +- test/bad-yield.reformatted-stderr | 10 +- ...iterator-invalid-method.reformatted-stderr | 4 +- ...erator-invalid-property.reformatted-stderr | 4 +- 9 files changed, 184 insertions(+), 667 deletions(-) create mode 100644 src/topiary/ursa.scm diff --git a/package.json b/package.json index 9047b5c..3d56cbb 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "argparse": "^2.0.1", "effection": "^3.0.3", "env-paths": "^3.0.0", + "execa": "^9.3.0", "fs-extra": "^11.2.0", "get-source": "^2.0.12", "ohm-js": "^17.1.0", @@ -43,7 +44,6 @@ "dir-compare": "^5.0.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^18.0.0", - "execa": "^9.3.0", "pre-push": "^0.1.4", "tree-kill": "^1.2.2", "ts-node": "^10.9.2", @@ -63,7 +63,7 @@ "scripts": { "lint": "eslint . --ext .ts && ts-unused-exports ./tsconfig.json --ignoreFiles=\"./src/grammar/ursa.ohm-bundle.*.ts\" && depcheck", "prebuild": "node --print \"'export default \\'' + require('./package.json').version + '\\';'\" > src/version.ts && npm run pre-compile-prelude", - "build": "tsc --build && npm run compile-prelude && mkdir -p lib/ark/compiler lib/ursa && cp src/ark/prelude.json lib/ark/ && cp src/ark/compiler/prelude.js lib/ark/compiler/ && cp src/ursa/prelude.ursa lib/ursa/", + "build": "tsc --build && npm run compile-prelude && mkdir -p lib/ark/compiler lib/ursa lib/topiary && cp src/ark/prelude.json lib/ark/ && cp src/ark/compiler/prelude.js lib/ark/compiler/ && cp src/ursa/prelude.ursa lib/ursa/ && cp src/topiary/ursa.scm lib/topiary/", "clean": "tsc --build --clean && rm -f src/grammar/ursa.ohm-bundle.ts src/grammar/ursa.ohm-bundle.d.ts", "generate-only": "ohm generateBundles --withTypes --esm 'src/grammar/*.ohm'", "patch-diff-ohm": "patch -p0 --output=src/grammar/ursa.ohm-bundle.d.part-patched.ts < src/grammar/ursa.ohm-bundle.d.ts.diff", @@ -92,7 +92,7 @@ }, "compile": "tsc" }, - "timeout": "30s", + "timeout": "60s", "workerThreads": false }, "pre-push": [ diff --git a/src/topiary/ursa.scm b/src/topiary/ursa.scm new file mode 100644 index 0000000..fe2ab89 --- /dev/null +++ b/src/topiary/ursa.scm @@ -0,0 +1,128 @@ +; Topiary queries for Ursa. + +; Sometimes we want to indicate that certain parts of our source text should +; not be formatted, but taken as-is. We use the leaf capture name to inform the +; tool of this. +[ + (raw_string_literal) + (string) +] @leaf + +; Allow blank line before +[ + (line_comment) + (block_comment) + (statement) + (member) +] @allow_blank_line_before + +; Surround spaces +[ + "and" + "else" + "in" + "or" + "=" + ":=" +] @prepend_space @append_space + +(binary_exp (_) _ @prepend_space @append_space (_)) + +; Append spaces +[ + "await" + "break" + (continue) + "for" + "if" + "launch" + "let" + "loop" + "not" + "return" + "use" + "var" + "yield" + ":" +] @append_space + +; Input softlines before all comments. This means that the input decides if +; a comment should have line breaks before. A line comment always ends with +; a line break. +[ + (block_comment) + (line_comment) + "else" +] @prepend_input_softline + +; Input softline after block comments unless followed by comma or semicolon, as +; they are always put directly after. +( + (block_comment) @append_input_softline + . + ["," ";"]* @do_nothing +) + +; Put on a separate line. If there is a comment following, we don't add anything, +; because the input softlines and spaces above will already have sorted out the +; formatting. +( + [ + (statement) + (member) + ] @prepend_input_softline @append_input_softline +) + +(line_comment) @append_hardline + +(block_comment) @multi_line_indent_all + +; Append softlines, unless followed by comments. +( + [ + "," + ";" + ] @append_spaced_softline + . + [(block_comment) (line_comment)]* @do_nothing +) + +; Prepend softlines before dots +(_ + "." @prepend_empty_softline +) + +; This patterns is duplicated for all nodes that can contain curly braces. +; Hoping to be able to generalise them like this: +; (_ +; . +; "{" @prepend_space +; (#for! block declaration_list enum_variant_list field_declaration_list) +; ) +; Perhaps even the built in #match! can do this + +;; fn +; (fn +; (identifier) @prepend_space +; ) + +(block + . + "{" @prepend_space +) + +(block + . + "{" @append_spaced_softline @append_indent_start + _ + "}" @prepend_spaced_softline @prepend_indent_end + . +) + +(object + . + "{" @append_spaced_softline @append_indent_start + _ + "}" @prepend_spaced_softline @prepend_indent_end + . +) diff --git a/src/ursa/cli.ts b/src/ursa/cli.ts index e615869..48d9de9 100644 --- a/src/ursa/cli.ts +++ b/src/ursa/cli.ts @@ -4,12 +4,12 @@ import assert from 'assert' import path from 'path' -import fs, {PathOrFileDescriptor} from 'fs-extra' import * as readline from 'readline' import {fileURLToPath} from 'url' import {ArgumentParser, RawDescriptionHelpFormatter} from 'argparse' import envPaths from 'env-paths' +import fs, {PathOrFileDescriptor} from 'fs-extra' import tildify from 'tildify' import {rollup} from 'rollup' import {nodeResolve} from '@rollup/plugin-node-resolve' @@ -34,6 +34,9 @@ import { } from '../ark/compiler/index.js' import {expToInst} from '../ark/flatten.js' +// eslint-disable-next-line @typescript-eslint/naming-convention +const __dirname = fileURLToPath(new URL('.', import.meta.url)) + if (process.env.DEBUG) { Error.stackTraceLimit = Infinity } @@ -98,9 +101,7 @@ const fmtParser = subparsers.add_parser('fmt', {aliases: ['f', 'format'], descri fmtParser.set_defaults({func: fmtCommand}) fmtParser.add_argument('source', {metavar: 'FILE', help: 'source code to format'}) fmtParser.add_argument('--output', '-o', {metavar: 'FILE', help: 'output file [default: standard output]'}) -fmtParser.add_argument('--width', {metavar: 'COLUMNS', help: 'maximum desired width of formatted code'}) fmtParser.add_argument('--indent', {metavar: 'STRING', help: 'indent string'}) -fmtParser.add_argument('--onelineFactor', {metavar: 'NUMBER', help: 'factor governing when expressions are wrapped (bigger means try to fit more complex expressions on one line) [default: 0]'}) interface Args { // Global arguments @@ -142,7 +143,7 @@ function getOutputFile( let prog: string // Use standard input if requested -function getInputFile(args: Args) { +function getInputFile(args: Args): PathOrFileDescriptor { let inputFile: PathOrFileDescriptor = args.source if (args.source !== '-') { prog = inputFile @@ -363,8 +364,6 @@ async function compileCommand(args: Args) { for (const k of recordKeys(runtimeContext)) { names.push(k) } - // eslint-disable-next-line @typescript-eslint/naming-convention - const __dirname = fileURLToPath(new URL('.', import.meta.url)) output += `import {${names.join(', ')}} from '${path.join(__dirname, '../../lib/ark/data.js')}'\n` output += `import {run, spawn} from '${path.join(__dirname, '../../node_modules/effection/esm/mod.js')}'\nlet prelude = ${prelude}; (await (run(prelude))).properties.forEach((val, sym) => jsGlobals.set(sym, val))\n` @@ -389,11 +388,12 @@ async function compileCommand(args: Args) { } function fmtCommand(args: Args) { - const outputFile = getOutputFile(args, true) const inputFile = getInputFile(args) - const source = readSourceFile(inputFile) - const output = format(source, args.width, args.indent, args.onelineFactor) + const input = fs.readFileSync(inputFile, {encoding: 'utf-8'}) + const output = format(input, args.indent) + const outputFile = getOutputFile(args, true) fs.writeFileSync(outputFile, output) + delete process.env.TOPIARY_LANGUAGE_DIR return Promise.resolve() } diff --git a/src/ursa/examples.test.ts b/src/ursa/examples.test.ts index 1a9e0b3..08c1cec 100644 --- a/src/ursa/examples.test.ts +++ b/src/ursa/examples.test.ts @@ -49,7 +49,7 @@ import { ['Test error on bad function call', 'test/bad-call'], ['Test error on invalid method invocation', 'test/sum-map-iterator-invalid-method'], ['Test error on invalid property access', 'test/sum-map-iterator-invalid-property'], - ['Test error on assignment to non-lvalue', 'test/bad-assignment'], + //['Test error on assignment to non-lvalue', 'test/bad-assignment'], ['Test error on re-assignment with wrong type', 'test/bad-reassignment'], ["Test error on duplicate identifiers in 'let'", 'test/duplicate-let'], ["Test error on 'yield' outside function", 'test/bad-yield'], diff --git a/src/ursa/fmt.ts b/src/ursa/fmt.ts index 4a8f1a7..561f7a0 100644 --- a/src/ursa/fmt.ts +++ b/src/ursa/fmt.ts @@ -2,648 +2,37 @@ // © Reuben Thomas 2023-2024 // Released under the GPL version 3, or (at your option) any later version. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import {debug} from '../ark/util.js' -import grammar, { - Node, NonterminalNode, IterationNode, ThisNode, - // eslint-disable-next-line import/extensions -} from '../grammar/ursa.ohm-bundle.js' +import fs from 'fs' +import path from 'path' +import {fileURLToPath} from 'url' -type FormatterOperations = { - fmt(a: FormatterArgs): Span -} - -type FormatterArgs = { - maxWidth: number - indentString: string - simpleExpDepth: number - horizontalOnly?: boolean -} - -type FormatterNode = Node -type FormatterNonterminalNode = NonterminalNode -type FormatterIterationNode = IterationNode -type FormatterThisNode = ThisNode<{a: FormatterArgs}, FormatterOperations> - -// eslint-disable-next-line max-len -const semantics = grammar.createSemantics() - -function depth(node: FormatterNode): number { - if (/^[a-z]/.test(node.ctorName)) { - return 0 - } - return Math.max( - ...node.children.map((node, _index, _array) => 1 + depth(node)), - ) -} - -type SpanContent = string | Span - -function narrowed(a: FormatterArgs): FormatterArgs { - return {...a, maxWidth: a.maxWidth - a.indentString.length} -} - -type SpanOpts = { - stringSep: string - indentString: string -} - -class Span { - protected options: SpanOpts - - constructor(protected content: SpanContent[], options: Partial = {}) { - this.options = { - stringSep: '', - indentString: '', - ...options, - } - } - - toString(): string { - const res = this.content.map((elem) => elem.toString()) - .filter((s) => s !== '') - .join(this.options.stringSep) - .replaceAll(this.options.stringSep, this.options.stringSep + this.options.indentString) - return res === '' ? '' : this.options.indentString + res - } - - width(): number { - return Math.max(...this.toString().split('\n').map((line) => line.length)) - } - - indent(indentString: string) { - this.options.indentString = indentString - return this - } -} - -type ListSpanOpts = { - addTrailingWhenVertical?: boolean -} - -class ListSpan extends Span { - protected listOptions: ListSpanOpts - - constructor( - content: SpanContent[], - private sep: string, - private spanMaker: (content: SpanContent[]) => Span, - options: Partial = {}, - ) { - super(content, options) - this.listOptions = { - addTrailingWhenVertical: false, - ...options, - } - } - - toString() { - const newContent = [] - for (const span of this.content) { - newContent.push(this.spanMaker([span, this.sep])) - } - if (this.content.length > 0 && !(this.listOptions.addTrailingWhenVertical && this.options.stringSep === '\n')) { - newContent.pop() - newContent.push(this.content[this.content.length - 1]) - } - return new Span(newContent, this.options).toString() - } -} - -function tightSpan(content: SpanContent[]) { - return new Span(content) -} - -function hSpan(content: SpanContent[]) { - return new Span(content, {stringSep: ' '}) -} - -function vSpan(content: SpanContent[]) { - return new Span(content, {stringSep: '\n'}) -} - -function tryFormats( - a: FormatterArgs, - hFormatter: (a: FormatterArgs) => Span, - vFormatters: ((a: FormatterArgs, span: Span) => Span)[], -): Span { - let res = hFormatter({...a, horizontalOnly: true}) - const width = res.width() - if (a.horizontalOnly || width <= a.maxWidth) { - return res - } - for (const f of vFormatters) { - res = f(a, res) - const width = res.width() - if (width <= a.maxWidth) { - break - } - } - return res -} - -function fmtIter(a: FormatterArgs, node: FormatterNonterminalNode): Span[] { - return node.asIteration().children.map((child) => child.fmt(a)) -} - -function fmtDelimitedList( - a: FormatterArgs, - openDelim: string, - closeDelim: string, - separator: string, - spanMaker: (content: SpanContent[]) => Span, - listNode: FormatterNonterminalNode, - vSeparator: string = separator, -) { - return tryFormats( - a, - () => new Span([ - openDelim, - new ListSpan(fmtIter(a, listNode), separator, spanMaker, {stringSep: ' '}), - closeDelim, - ]), - [() => new Span([ - openDelim, '\n', - new ListSpan(fmtIter(narrowed(a), listNode), vSeparator, spanMaker, {stringSep: '\n', indentString: a.indentString, addTrailingWhenVertical: true}), - '\n', closeDelim, - ]), - () => new Span([ - openDelim, '\n', - new ListSpan(fmtIter(narrowed(a), listNode), vSeparator, spanMaker, {stringSep: '\n', indentString: a.indentString, addTrailingWhenVertical: true}), - '\n', closeDelim, - ], {stringSep: '\n'})], - ) -} - -function fmtIndented( - a: FormatterArgs, - listNode: FormatterNonterminalNode, -): Span { - const narrowedA = narrowed(a) - return vSpan( - listNode.asIteration().children.map((child) => child.fmt(narrowedA)), - ).indent(a.indentString) -} +import {execaSync} from 'execa' +import tmp from 'tmp' -function fmtUnary( - a: FormatterArgs, - op: string, - spanMaker: (content: (string | Span)[]) => Span, - node: FormatterNonterminalNode, -): Span { - return tryFormats( - a, - () => spanMaker([op, node.fmt(a)]), - [(a) => vSpan([new Span([op, '(']), node.fmt(narrowed(a)).indent(a.indentString), ')'])], - ) -} - -function fmtBinary( - a: FormatterArgs, - op: string, - left: FormatterNonterminalNode, - right: FormatterNonterminalNode, -): Span { - return tryFormats( - a, - () => hSpan([left.fmt(a), op, right.fmt(a)]), - [(a) => vSpan([ - '(', - vSpan([left.fmt(narrowed(a)), hSpan([op, right.fmt(narrowed(a))])]).indent(a.indentString), - ')', - ])], - ) -} - -function fmtIfs( - a: FormatterArgs, - ifs: FormatterNonterminalNode, - elseBlock: FormatterNonterminalNode, -): Span { - const formattedIfs = ifs.asIteration().children.map((child) => child.fmt(a)) - if (elseBlock.children.length > 0) { - const formattedElse = elseBlock.children[0].fmt(a) - formattedIfs.push(formattedElse) - } - return new ListSpan(formattedIfs, 'else', hSpan, {stringSep: '\n'}) -} - -// Return list of 0 or 1 Spans. -function fmtOptional(a: FormatterArgs, maybeNode: FormatterNonterminalNode) { - if (maybeNode.children.length > 0) { - return [maybeNode.children[0].fmt(a)] - } - return [] -} - -function fmtMaybeType( - a: FormatterArgs, - innerSpans: SpanContent[], - maybeType: FormatterIterationNode, -) { - if (maybeType.children.length > 0) { - innerSpans.push(':') - } - const outerSpans = [new Span(innerSpans)] - if (maybeType.children.length > 0) { - outerSpans.push(maybeType.children[0].children[1].fmt(a)) - } - return outerSpans -} - -semantics.addOperation('fmt(a)', { - _terminal() { - return new Span([this.sourceString]) - }, - identName(_start, _rest) { - return new Span([this.sourceString]) - }, - - // Horizontal output of short sequences is handled by the Block rule. - Sequence(exps, _sc) { - return vSpan(exps.asIteration().children.map((child) => child.fmt(this.args.a))) - }, - - PrimaryExp_paren(_open, exp, _close) { - return tryFormats( - this.args.a, - (a) => new Span(['(', hSpan([exp.fmt(a)]), ')']), - [(a) => vSpan(['(', vSpan([exp.fmt(narrowed(a))]).indent(a.indentString), ')'])], - ) - }, - - Definition(ident, initializer) { - return hSpan([ident.fmt(this.args.a), initializer.fmt(this.args.a)]) - }, - Initializer(_equals, value) { - return hSpan(['=', value.fmt(this.args.a)]) - }, - - List(_open, elems, _maybeComma, _close) { - return fmtDelimitedList(this.args.a, '[', ']', ',', tightSpan, elems) - }, - - Map(_open, elems, _maybeComma, _close) { - return fmtDelimitedList(this.args.a, '{', '}', ',', tightSpan, elems) - }, - KeyValue(key, _colon, value) { - return tryFormats( - this.args.a, - (a) => hSpan([new Span([key.fmt(a), ':']), value.fmt(a)]), - [(a) => vSpan([new Span([key.fmt(a), ':']), value.fmt(a)])], - ) - }, - - Object(maybeType, _open, elems, _maybeComma, _close) { - return hSpan([ - ...fmtOptional(this.args.a, maybeType), - fmtDelimitedList(this.args.a, '{', '}', ';', tightSpan, elems, ''), - ]) - }, - - PostfixExp_property(exp, _dot, ident) { - return tryFormats( - this.args.a, - (a) => new Span([exp.fmt(a), '.', ident.fmt(a)]), - [(a) => vSpan([exp.fmt(a), new Span(['.', ident.fmt(a)])])], - ) - }, - PostfixExp_invoke(exp, _dot, property, _spaces, _open, args, _maybeComma, _close) { - return tryFormats( - this.args.a, - (a) => new Span([exp.fmt(a), new Span(['.', property.fmt(a), '(']), fmtDelimitedList(a, '', ')', ',', tightSpan, args)]), - [(a) => vSpan([exp.fmt(a), new Span(['.', property.fmt(a), '(']), fmtDelimitedList(a, '', ')', ',', tightSpan, args)])], - ) - }, - PostfixExp_call(exp, _spaces, _open, args, _maybeComma, _close) { - return tryFormats( - this.args.a, - (a) => new Span([new Span([exp.fmt(a), '(']), fmtDelimitedList(a, '', ')', ',', tightSpan, args)]), - [(a) => vSpan([new Span([exp.fmt(a), '(']), fmtDelimitedList(a, '', ')', ',', tightSpan, args)])], - ) - }, - - Ifs(ifs, _else, elseBlock) { - return fmtIfs(this.args.a, ifs, elseBlock) - }, - If(_if, cond, thenBlock) { - return tryFormats( - this.args.a, - (a) => hSpan(['if', cond.fmt(a), thenBlock.fmt(a)]), - [(a) => vSpan(['if', cond.fmt(a), thenBlock.fmt(a)])], - ) - }, - - Fn(type, body) { - return tryFormats( - this.args.a, - (a) => hSpan([type.fmt(a), body.fmt(a)]), - [(a) => vSpan([type.fmt(a), body.fmt(a)])], - ) - }, - FnType(fn, _open, params, _maybeComma, _close, maybeType) { - return tryFormats( - this.args.a, - (a) => hSpan(fmtMaybeType( - a, - [fn.ctorName, fmtDelimitedList(a, '(', ')', ',', tightSpan, params)], - maybeType, - )), - [(a) => vSpan(fmtMaybeType( - a, - [fn.ctorName, fmtDelimitedList(a, '(', ')', ',', tightSpan, params)], - maybeType, - )), - ], - ) - }, - Param(ident, maybeType) { - return hSpan(fmtMaybeType( - this.args.a, - [ident.fmt(this.args.a)], - maybeType, - )) - }, - - Loop(_loop, body) { - return hSpan(['loop', body.fmt(this.args.a)]) - }, - - For(_for, ident, _in, iterator, body) { - return tryFormats( - this.args.a, - (a) => hSpan(['for', ident.fmt(a), 'in', iterator.fmt(a), body.fmt(a)]), - [(a) => vSpan([ - hSpan(['for', ident.fmt(a), 'in', iterator.fmt(a)]), - vSpan([body.fmt(a)])])], - ) - }, - - UnaryExp_bitwise_not(_not, exp) { - return fmtUnary(this.args.a, '~', tightSpan, exp) - }, - UnaryExp_pos(_plus, exp) { - return fmtUnary(this.args.a, '+', tightSpan, exp) - }, - UnaryExp_neg(_neg, exp) { - return fmtUnary(this.args.a, '-', tightSpan, exp) - }, - - ExponentExp_power(left, _power, right) { - return fmtBinary(this.args.a, '**', left, right) - }, - - ProductExp_times(left, _times, right) { - return fmtBinary(this.args.a, '*', left, right) - }, - ProductExp_divide(left, _divide, right) { - return fmtBinary(this.args.a, '/', left, right) - }, - ProductExp_mod(left, _mod, right) { - return fmtBinary(this.args.a, '%', left, right) - }, - - SumExp_plus(left, _plus, right) { - return fmtBinary(this.args.a, '+', left, right) - }, - SumExp_minus(left, _minus, right) { - return fmtBinary(this.args.a, '-', left, right) - }, - - CompareExp_eq(left, _eq, right) { - return fmtBinary(this.args.a, '==', left, right) - }, - CompareExp_neq(left, _neq, right) { - return fmtBinary(this.args.a, '!=', left, right) - }, - CompareExp_lt(left, _lt, right) { - return fmtBinary(this.args.a, '<', left, right) - }, - CompareExp_leq(left, _leq, right) { - return fmtBinary(this.args.a, '<=', left, right) - }, - CompareExp_gt(left, _gt, right) { - return fmtBinary(this.args.a, '>', left, right) - }, - CompareExp_geq(left, _ge, right) { - return fmtBinary(this.args.a, '>=', left, right) - }, - - BitwiseExp_and(left, _and, right) { - return fmtBinary(this.args.a, '&', left, right) - }, - BitwiseExp_or(left, _or, right) { - return fmtBinary(this.args.a, '|', left, right) - }, - BitwiseExp_xor(left, _xor, right) { - return fmtBinary(this.args.a, '^', left, right) - }, - BitwiseExp_lshift(left, _lshift, right) { - return fmtBinary(this.args.a, '<<', left, right) - }, - BitwiseExp_arshift(left, _arshift, right) { - return fmtBinary(this.args.a, '>>', left, right) - }, - BitwiseExp_lrshift(left, _lrshift, right) { - return fmtBinary(this.args.a, '>>>', left, right) - }, - - LogicNotExp_not(_not, exp) { - return fmtUnary(this.args.a, 'not', hSpan, exp) - }, - - LogicExp_and(left, _and, right) { - return fmtBinary(this.args.a, 'and', left, right) - }, - LogicExp_or(left, _or, right) { - return fmtBinary(this.args.a, 'or', left, right) - }, - - Assignment_ass(lvalue, _ass, exp) { - return fmtBinary(this.args.a, ':=', lvalue, exp) - }, - - Exp_await(_await, exp) { - return tryFormats( - this.args.a, - (a) => hSpan(['await', exp.fmt(a)]), - [(a) => hSpan(['await', exp.fmt(a)])], - ) - }, - - Exp_yield(_yield, exp) { - return tryFormats( - this.args.a, - (a) => hSpan(['yield', ...fmtOptional(a, exp)]), - [(a) => hSpan(['yield', ...fmtOptional(a, exp)])], - ) - }, - - Exp_launch(_launch, exp) { - return tryFormats( - this.args.a, - (a) => hSpan(['launch', exp.fmt(a)]), - [(a) => hSpan(['launch', exp.fmt(a)])], - ) - }, - - Statement_break(_break, exp) { - return tryFormats( - this.args.a, - (a) => hSpan(['break', ...fmtOptional(a, exp)]), - [(a) => hSpan(['break', ...fmtOptional(a, exp)])], - ) - }, - Statement_continue(_continue) { - return hSpan(['continue']) - }, - Statement_return(_return, exp) { - return tryFormats( - this.args.a, - (a) => hSpan(['return', ...fmtOptional(a, exp)]), - [(a) => hSpan(['return', ...fmtOptional(a, exp)])], - ) - }, - - Lets(lets) { - return tryFormats( - this.args.a, - (a) => new ListSpan(fmtIter(a, lets), 'and', hSpan, {stringSep: ' '}), - [(a) => new ListSpan(fmtIter(a, lets), 'and', hSpan, {stringSep: ' '})], - ) - }, - Let(let_, definition) { - return tryFormats( - this.args.a, - (a) => hSpan([let_.ctorName, definition.fmt(a)]), - [(a) => vSpan([let_.ctorName, definition.fmt(a)])], - ) - }, - - Use(_use, path) { - return tryFormats( - this.args.a, - (a) => hSpan(['use', path.fmt(a)]), - [(a) => vSpan(['use', path.fmt(a)])], - ) - }, - - Block(_open, seq, _close) { - if (seq.children[0].asIteration().children.length === 1) { - const exp = seq.children[0].asIteration().children[0] - if (exp.ctorName === 'Exp' && depth(exp) < this.args.a.simpleExpDepth) { - return new Span(['{', seq.fmt(this.args.a), '}']) - } - } - return vSpan(['{', fmtIndented(this.args.a, seq.children[0]), '}']) - }, - - NamedType(path, maybeTypeArgs) { - return hSpan([path.fmt(this.args.a), ...fmtOptional(this.args.a, maybeTypeArgs)]) - }, - Type_intersection(typeList) { - return tryFormats( - this.args.a, - (a) => new ListSpan(fmtIter(a, typeList), '+', hSpan), - [(a) => new ListSpan(fmtIter(a, typeList), '+', vSpan)], - ) - }, - TypeParams(_open, params, _maybeCommaAngle, _close) { - return fmtDelimitedList(this.args.a, '<', '>', ',', hSpan, params) - }, - TypeParam(ident, maybeType) { - return hSpan(fmtMaybeType( - this.args.a, - [ident.fmt(this.args.a)], - maybeType, - )) - }, - TypeArgs(_open, args, _maybeComma, _close) { - return fmtDelimitedList(this.args.a, '<', '>', ',', hSpan, args) - }, - - Class(_class, ident, typeParams, type, _open, members, _sc, _close) { - return hSpan([ - 'class', - ident.fmt(this.args.a), - new Span([typeParams.fmt(this.args.a), ':']), - type.children[1].fmt(this.args.a), - vSpan(['{', fmtIndented(this.args.a, members), '}']), - ]) - }, - ClassField(maybePub, maybeStatic, maybeVar, ident, maybeType, maybeInit) { - return hSpan([ - ...fmtOptional(this.args.a, maybePub), - ...fmtOptional(this.args.a, maybeStatic), - ...fmtOptional(this.args.a, maybeVar), - ...fmtMaybeType(this.args.a, [ident.fmt(this.args.a)], maybeType), - ...fmtOptional(this.args.a, maybeInit), - ]) - }, - ClassMethod(maybePub, maybeStatic, ident, maybeTypeParams, _eq, fn) { - return hSpan([ - ...fmtOptional(this.args.a, maybePub), - ...fmtOptional(this.args.a, maybeStatic), - ident.fmt(this.args.a), - ...fmtOptional(this.args.a, maybeTypeParams), - '=', - fn.fmt(this.args.a), - ]) - }, - - Trait(_trait, ident, typeParams, type, _open, members, _sc, _close) { - return hSpan([ - 'trait', - ident.fmt(this.args.a), - typeParams.fmt(this.args.a), - ':', - type.children[1].fmt(this.args.a), - vSpan(['{', fmtIndented(this.args.a, members), '}']), - ]) - }, - TraitField(maybeVar, ident, type) { - const spans = [] - if (maybeVar.children.length > 0) { - spans.push('var') - } - return hSpan([...spans, ident.fmt(this.args.a), type.fmt(this.args.a)]) - }, - TraitMethod(ident, maybeTypeParams, fnType) { - return hSpan([ - ident.fmt(this.args.a), - new Span([hSpan(fmtOptional(this.args.a, maybeTypeParams)), ':']), - fnType.fmt(this.args.a), - ]) - }, - - Path(pathList) { - return new ListSpan(fmtIter(this.args.a, pathList), '.', tightSpan) - }, - - number(_) { - return new Span([this.sourceString]) - }, - - string(_open, _str, _close) { - return new Span([this.sourceString]) - }, - - literalString(_open, _str, _close) { - return new Span([this.sourceString]) - }, -}) +// eslint-disable-next-line @typescript-eslint/naming-convention +const __dirname = fileURLToPath(new URL('.', import.meta.url)) export function format( expr: string, - maxWidth: number = 80, indentString: string = ' ', - simpleExpDepth: number = 0, - startRule?: string, ): string { - const matchResult = grammar.match(expr, startRule) - if (matchResult.failed()) { - throw new Error(matchResult.message) - } - const ast = semantics(matchResult) - return `${ast.fmt({maxWidth, indentString, simpleExpDepth})}\n` + const tmpConfigFile = tmp.fileSync({keep: true}) + fs.writeFileSync(tmpConfigFile.fd, `\ +{ + languages = { + ursa = { + extensions = ["ursa"], + indent | priority 1 = "${indentString}", + }, + }, +}`) + process.env.TOPIARY_LANGUAGE_DIR = path.join(__dirname, '../../lib/topiary') + const result = execaSync( + 'topiary', + ['format', '--language', 'ursa', '--configuration', tmpConfigFile.name], + {input: expr, stripFinalNewline: false}, + ) + tmpConfigFile.removeCallback() + delete process.env.TOPIARY_LANGUAGE_DIR + return result.stdout } diff --git a/test/bad-call.reformatted-stderr b/test/bad-call.reformatted-stderr index e3cac44..4cde2fb 100644 --- a/test/bad-call.reformatted-stderr +++ b/test/bad-call.reformatted-stderr @@ -1,13 +1,13 @@ -Error: Line 3, col 5: - 2 | let g = fn(): Int { -> 3 | h() - ^~~ - 4 | } +Error: Line 2, col 21: + 1 | let h = 3 +> 2 | let g = fn(): Int { h() } + ^ + 3 | let f = fn(): Int { g() } Invalid call Traceback (most recent call last) - line 6 - g(), in f - line 8 + line 3 + let f = fn(): Int { g() }, in f + line 4 f(), at top level \ No newline at end of file diff --git a/test/bad-yield.reformatted-stderr b/test/bad-yield.reformatted-stderr index 861bfdb..6843de8 100644 --- a/test/bad-yield.reformatted-stderr +++ b/test/bad-yield.reformatted-stderr @@ -1,7 +1,7 @@ -Error: Line 6, col 13: - 5 | if i <= n { -> 6 | yield i - 1 - ^ - 7 | } else +Error: Line 5, col 21: + 4 | i := i + 1 +> 5 | if i <= n { yield i - 1 } else { return null } + ^ + 6 | } yield may only be used in a generator \ No newline at end of file diff --git a/test/sum-map-iterator-invalid-method.reformatted-stderr b/test/sum-map-iterator-invalid-method.reformatted-stderr index 77b9613..0235ed2 100644 --- a/test/sum-map-iterator-invalid-method.reformatted-stderr +++ b/test/sum-map-iterator-invalid-method.reformatted-stderr @@ -2,10 +2,10 @@ Error: Line 6, col 17: 5 | let l = it() > 6 | let k = l.get(0) and let v = l.get(1) ^~~~~~~~ - 7 | if l == null { + 7 | if l == null { return tot } Invalid method Traceback (most recent call last) - line 14 + line 12 sum({"a": 10, "b": 30, "c": 50, "d": 5, "e": 5}), at top level \ No newline at end of file diff --git a/test/sum-map-iterator-invalid-property.reformatted-stderr b/test/sum-map-iterator-invalid-property.reformatted-stderr index f9c8380..9658700 100644 --- a/test/sum-map-iterator-invalid-property.reformatted-stderr +++ b/test/sum-map-iterator-invalid-property.reformatted-stderr @@ -2,10 +2,10 @@ Error: Line 6, col 17: 5 | let l = it() > 6 | let k = l.get ^~~~~ - 7 | if l == null { + 7 | if l == null { return tot } Invalid property Traceback (most recent call last) - line 13 + line 11 sum({"a": 10, "b": 30, "c": 50, "d": 5, "e": 5}), at top level \ No newline at end of file