From cf779881864f4f1de676491555be4eec1c7e3230 Mon Sep 17 00:00:00 2001 From: uakci Date: Sun, 12 May 2024 22:12:01 +0200 Subject: [PATCH 1/3] cli: add tree-kdl subcommand --- package-lock.json | 56 +++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/cli.ts | 14 +++++++++++ src/modes/kdl.ts | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 src/modes/kdl.ts diff --git a/package-lock.json b/package-lock.json index 2d73f93..2ed0966 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "better-react-mathjax": "^2.0.3", "canvas": "^2.11.2", "discord.js": "^14.13.0", + "kdljs": "^0.2.0", "lodash": "^4.17.21", "mathjax": "^3.2.2", "mathjax-full": "^3.2.2", @@ -384,6 +385,35 @@ "node": ">=6.9.0" } }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==" + }, "node_modules/@discordjs/builders": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.8.1.tgz", @@ -1516,6 +1546,19 @@ "node": "*" } }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -2014,6 +2057,14 @@ "node": ">=6" } }, + "node_modules/kdljs": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/kdljs/-/kdljs-0.2.0.tgz", + "integrity": "sha512-wZVx1drSL7aZ/6K6kUcR3IvIQbgjioWlpWZC/IlEdSoLh4iUQM85ObOs+8rVVaIxUkn2vjtFq9UbdYWKQUgKJA==", + "dependencies": { + "chevrotain": "^10.1.2" + } + }, "node_modules/local-pkg": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", @@ -2550,6 +2601,11 @@ "node": ">= 6" } }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index bd2664f..366ea50 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "better-react-mathjax": "^2.0.3", "canvas": "^2.11.2", "discord.js": "^14.13.0", + "kdljs": "^0.2.0", "lodash": "^4.17.21", "mathjax": "^3.2.2", "mathjax-full": "^3.2.2", diff --git a/src/cli.ts b/src/cli.ts index f8f923a..3578b91 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,6 +9,8 @@ import { trimTree } from './tree/trim'; import { drawTreeToCanvas } from './tree/draw'; import { parse } from './modes/parse'; import { textual_tree_from_json } from './modes/textual-tree'; +import KDL from 'kdljs'; +import { formatTreeAsKdl } from './modes/kdl'; import { testSentences } from './modes/test-sentences'; import { denote } from './semantics/denote'; import { ToaqTokenizer } from './morphology/tokenize'; @@ -152,6 +154,18 @@ yargs console.log(JSON.stringify(trees)); }, ) + .command( + 'tree-kdl', + 'List of parse trees in KDL format', + yargs => { + yargs.demandOption('sentence'); + }, + + function (argv) { + const trees = getTrees(argv); + console.log(KDL.format(trees.map(formatTreeAsKdl))); + }, + ) .command( 'tree-text', 'List of parse trees in plain text format', diff --git a/src/modes/kdl.ts b/src/modes/kdl.ts new file mode 100644 index 0000000..2ce8146 --- /dev/null +++ b/src/modes/kdl.ts @@ -0,0 +1,60 @@ +import KDL from 'kdljs'; +import { Tree } from '../tree'; + +const toStringRecord = (obj: O) => { + for (const key in obj) { + if (!Object.hasOwn(obj, key)) continue; + if (obj[key] === undefined) delete obj[key]; + else (obj as Record)[key] = String(obj[key]); + } + return obj as { [k in keyof O]: O[k] extends undefined ? never : string }; +}; + +export interface PartialKdlNode { + name: string; + values?: any[]; + properties?: {}; + children?: PartialKdlNode[]; + tags?: { + name?: string; + values?: any[]; + properties?: {}; + }; +} + +export const kdlNode = (partialNode: PartialKdlNode): KDL.Node => ({ + name: partialNode.name, + values: toStringRecord(partialNode.values ?? []), + properties: toStringRecord(partialNode.properties ?? {}), + children: partialNode.children?.map(kdlNode) ?? [], + tags: { + // kdljs type stubs define this type too strictly; the API itself does + // accept undefined here + name: partialNode.tags?.name ?? (undefined as unknown as string), + values: toStringRecord(partialNode.tags?.values ?? []), + properties: toStringRecord(partialNode.tags?.properties ?? {}), + }, +}); + +export function formatTreeAsKdl(tree: Tree): KDL.Node { + const children = + 'word' in tree + ? [] + : 'children' in tree + ? tree.children + : [tree.left, tree.right]; + + return kdlNode({ + name: tree.label, + children: children.map(formatTreeAsKdl), + values: + 'word' in tree + ? ['value' in tree.word ? tree.word.value : tree.word.text] + : [], + properties: { + binding: tree.binding, + coindex: tree.coindex, + ...('word' in tree ? { id: tree.id, movedTo: tree.movedTo } : {}), + }, + }); +} From ebe0bc0f569efef6a184987f5c0e0eb6a19d274e Mon Sep 17 00:00:00 2001 From: uakci Date: Mon, 13 May 2024 20:10:22 +0200 Subject: [PATCH 2/3] sema/render: parameterize, add json mode + pprint --- src/cli.ts | 45 +++++++ src/semantics/render.ts | 271 ++++++++++++++++++++++++++++++---------- 2 files changed, 248 insertions(+), 68 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 3578b91..b5efb7c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,6 +16,13 @@ import { denote } from './semantics/denote'; import { ToaqTokenizer } from './morphology/tokenize'; import { toEnglish } from './english/tree'; import { denotationRenderText } from './tree/place'; +import { DTree } from './semantics/model'; +import { + toPlainText, + toLatex, + toJson, + jsonStringifyCompact, +} from './semantics/render'; function getTrees(argv: { sentence: string | undefined; @@ -197,6 +204,44 @@ yargs fs.writeFileSync(argv.output as string, toDocument(trees)); }, ) + .command( + 'denote', + 'Full denotation in given format', + yargs => { + yargs.demandOption('sentence'); + yargs.option('format', { + type: 'string', + choices: ['text', 'latex', 'json'], + default: 'text', + }); + }, + + function (argv) { + const dtrees = getTrees({ ...argv, semantics: true }) as DTree[]; + const format = argv.format as 'text' | 'latex' | 'json'; + switch (format) { + case 'text': + for (const dtree of dtrees) { + console.log(toPlainText(dtree.denotation!)); + } + break; + case 'latex': + for (const dtree of dtrees) { + console.log(toLatex(dtree.denotation!)); + } + break; + case 'json': + console.log( + jsonStringifyCompact( + dtrees.map(({ denotation }) => toJson(denotation!)), + ), + ); + break; + default: + format satisfies never; + } + }, + ) .command( 'test-sentences', 'Test parsing many sentences', diff --git a/src/semantics/render.ts b/src/semantics/render.ts index c793397..e7e8555 100644 --- a/src/semantics/render.ts +++ b/src/semantics/render.ts @@ -1,3 +1,4 @@ +import { values } from 'lodash'; import { Impossible } from '../core/error'; import { CompactExpr } from './compact'; import { Expr, ExprType } from './model'; @@ -101,42 +102,40 @@ const infixAssociativity: Record<(Expr & { head: 'infix' })['name'], boolean> = coevent: false, }; +type Quantifier = (Expr & { head: 'quantifier' })['name'] | 'lambda'; +type Infix = (Expr & { head: 'infix' })['name']; +type Polarizer = (Expr & { head: 'polarizer' })['name']; +type Constant = (Expr & { head: 'constant' })['name']; + /** * Specification of a rendering format, such as plain text or LaTeX. */ -interface Format { - bracket: (e: string) => string; - name: (name: Name) => string; - verb: (name: string, args: string[], event: string, world: string) => string; - quantifierSymbols: Record< - (Expr & { head: 'quantifier' })['name'] | 'lambda', - string - >; - quantifier: (symbol: string, name: string, body: string) => string; - restrictedQuantifier: ( - symbol: string, - name: string, - restriction: string, - body: string, - ) => string; +interface Format { + bracket: (e: T) => T; + name: (name: Name) => T; + verb: (name: string, args: T[], event: T, world: T) => T; + symbolForQuantifier: (symbol: Quantifier) => T; + quantifier: (symbol: T, name: T, body: T) => T; + restrictedQuantifier: (symbol: T, name: T, restriction: T, body: T) => T; + aspect: (infix: T, right: T) => T; eventCompound: ( - symbol: string, + symbol: T, verbName: string, - event: string, - world: string, - aspect: string, - agent: string | undefined, - args: string[], - body: string | undefined, - ) => string; - apply: (fn: string, argument: string) => string; - presuppose: (body: string, presupposition: string) => string; - infixSymbols: Record<(Expr & { head: 'infix' })['name'], string>; - infix: (symbol: string, left: string, right: string) => string; - polarizerSymbols: Record<(Expr & { head: 'polarizer' })['name'], string>; - polarizer: (symbol: string, body: string) => string; - constantSymbols: Record<(Expr & { head: 'constant' })['name'], string>; - quote: (text: string) => string; + event: T, + world: T, + aspect: T, + agent: T | undefined, + args: T[], + body: T | undefined, + ) => T; + apply: (fn: T, argument: T) => T; + presuppose: (body: T, presupposition: T) => T; + symbolForInfix: (symbol: Infix) => T; + infix: (symbol: T, left: T, right: T) => T; + symbolForPolarizer: (symbol: Polarizer) => T; + polarizer: (symbol: T, body: T) => T; + symbolForConstant: (symbol: Constant) => T; + quote: (text: string) => T; } const formatName = (name: Name) => { @@ -147,7 +146,12 @@ const formatName = (name: Name) => { ); }; -const plainText: Format = { +const fnFromMap = + (extension: Record) => + (t: T): U => + extension[t]; + +const plainText: Format = { bracket: e => `(${e})`, name: name => { const base = formatName(name); @@ -157,17 +161,18 @@ const plainText: Format = { args.length === 0 ? `${name}.${world}(${event})` : `${name}.${world}(${args.join(', ')})(${event})`, - quantifierSymbols: { + symbolForQuantifier: fnFromMap({ some: '∃', every: '∀', every_sing: '∀.SING ', every_cuml: '∀.CUML ', gen: 'GEN ', lambda: 'λ', - }, + }), quantifier: (symbol, name, body) => `${symbol}${name}. ${body}`, restrictedQuantifier: (symbol, name, restriction, body) => `${symbol}${name} : ${restriction}. ${body}`, + aspect: (infix, right) => infix + right, eventCompound: ( symbol, verbName, @@ -187,7 +192,7 @@ const plainText: Format = { }, apply: (fn, argument) => `${fn}(${argument})`, presuppose: (body, presupposition) => `${body} | ${presupposition}`, - infixSymbols: { + symbolForInfix: fnFromMap({ and: '∧', or: '∨', equals: '=', @@ -198,14 +203,14 @@ const plainText: Format = { after_near: '>.near', roi: '&', coevent: 'o', - }, + }), infix: (symbol, left, right) => `${left} ${symbol} ${right}`, - polarizerSymbols: { + symbolForPolarizer: fnFromMap({ not: '¬', indeed: '†', - }, + }), polarizer: (symbol, body) => `${symbol}${body}`, - constantSymbols: { + symbolForConstant: fnFromMap({ ji: 'jí', suq: 'súq', nhao: 'nháo', @@ -235,11 +240,11 @@ const plainText: Format = { promise: 'PROMISE', permit: 'PERMIT', warn: 'WARN', - }, + }), quote: text => `“${text}”`, }; -const latex: Format = { +const latex: Format = { bracket: e => `\\left(${e}\\right)`, name: name => { const base = formatName(name); @@ -249,17 +254,18 @@ const latex: Format = { args.length === 0 ? `\\mathrm{${name}}_{${world}}(${event})` : `\\mathrm{${name}}_{${world}}(${args.join(', ')})(${event})`, - quantifierSymbols: { + symbolForQuantifier: fnFromMap({ some: '\\exists', every: '\\forall', every_sing: '\\forall_{\\mathrm{\\large SING}}', every_cuml: '\\forall_{\\mathrm{\\large CUML}}', gen: '\\mathrm{\\large GEN}\\ ', lambda: '\\lambda', - }, + }), quantifier: (symbol, name, body) => `${symbol} ${name}.\\ ${body}`, restrictedQuantifier: (symbol, name, restriction, body) => `${symbol} ${name} : ${restriction}.\\ ${body}`, + aspect: (infix, right) => infix + right, eventCompound: ( symbol, verbName, @@ -279,7 +285,7 @@ const latex: Format = { }, apply: (fn, argument) => `${fn}(${argument})`, presuppose: (body, presupposition) => `${body}\\ |\\ ${presupposition}`, - infixSymbols: { + symbolForInfix: fnFromMap({ and: '\\land{}', or: '\\lor{}', equals: '=', @@ -290,14 +296,14 @@ const latex: Format = { after_near: '>_{\\text{near}}', roi: '&', coevent: '\\operatorname{o}', - }, + }), infix: (symbol, left, right) => `${left} ${symbol} ${right}`, - polarizerSymbols: { + symbolForPolarizer: fnFromMap({ not: '\\neg', indeed: '\\dagger', - }, + }), polarizer: (symbol, body) => `${symbol} ${body}`, - constantSymbols: { + symbolForConstant: fnFromMap({ ji: '\\text{jí}', suq: '\\text{súq}', nhao: '\\text{nháo}', @@ -327,10 +333,111 @@ const latex: Format = { promise: '\\mathrm{P\\large ROMISE}', permit: '\\mathrm{P\\large ERMIT}', warn: '\\mathrm{W\\large ARN}', - }, + }), quote: text => `“${text}”`, }; +// TypeScript won't allow { ['a' | 'b']: 'c' } for { a: 'c' } | { b: 'c' }, but it +// will happily split a union type case-by-case in a mapped type and as a map index. +type OneKeyAmong = { + [key in Keys]: { [_ in key]: Value }; +}[Keys]; + +type JsonAspect = { infix: string; right: JsonExpr }; + +export type JsonExpr = + | { variable: string } + | { constant: string } + | { quote: string } + | { verb: string; event: JsonExpr; world: JsonExpr; args: JsonExpr[] } + | JsonQuantifierExpr + | { + compound: string; + event: JsonExpr; + world: JsonExpr; + aspect: JsonAspect; + agent?: JsonExpr; + args: JsonExpr[]; + body?: JsonExpr; + } + | { apply: JsonExpr; to: JsonExpr } + | { claim: JsonExpr; presupposing: JsonExpr } + | { infix: Infix; left: JsonExpr; right: JsonExpr } + | JsonPolarizerExpr; + +type JsonQuantifierExpr = OneKeyAmong & { + restriction?: JsonExpr; + body: JsonExpr; +}; + +// TypeScript will complain about a circular type definition if we use +// OneKeyAmong here (possibly because there's no &-intersection-type +// refinement). +type JsonPolarizerExpr = { + [polarizer in Polarizer]: { [_ in polarizer]: JsonExpr }; +}[Polarizer]; + +type JsonExprIntermediate = JsonExpr | JsonAspect | string; + +const json: Format = { + bracket: expr => expr, + name: ({ id, type }: Name) => ({ variable: `${type}${id}` }), + verb: (verb, args, event, world) => ({ + verb, + args: args as JsonExpr[], + event: event as JsonExpr, + world: world as JsonExpr, + }), + symbolForQuantifier: quantifier => quantifier, + quantifier: (symbol, name, body) => + ({ + [symbol as Quantifier]: name as string, + body: body as JsonExpr, + }) as JsonQuantifierExpr, + restrictedQuantifier: (symbol, name, restriction, body) => + Object.assign(json.quantifier(symbol, name, body), { + restriction: restriction as JsonExpr, + }), + aspect: (infix, right) => ({ + infix: infix as string, + right: right as JsonExpr, + }), + eventCompound: ( + _symbol, + verbName, + event, + world, + aspect, + agent, + args, + body, + ) => ({ + compound: verbName, + event: event as JsonExpr, + world: world as JsonExpr, + aspect: aspect as JsonAspect, + args: args as JsonExpr[], + ...(agent && { agent: agent as JsonExpr }), + ...(body && { body: body as JsonExpr }), + }), + apply: (apply, to) => ({ apply: apply as JsonExpr, to: to as JsonExpr }), + presuppose: (claim, presupposing) => ({ + claim: claim as JsonExpr, + presupposing: presupposing as JsonExpr, + }), + symbolForInfix: infix => infix, + infix: (infix, left, right) => ({ + infix: infix as Infix, + left: left as JsonExpr, + right: right as JsonExpr, + }), + symbolForPolarizer: polarizer => polarizer, + polarizer: (polarizer, body) => + ({ [polarizer as Polarizer]: body as JsonExpr }) as JsonPolarizerExpr, + symbolForConstant: constant => ({ constant }), + quote: quote => ({ quote }), +}; + /** * Adds a new name of type 'type' to the given naming context. */ @@ -358,7 +465,7 @@ function addName(type: ExprType, names: Names, constant = false): Names { }; } -function getName(index: number, names: Names, fmt: Format): string { +function getName(index: number, names: Names, fmt: Format): T { return fmt.name(names.context[index]); } @@ -372,13 +479,13 @@ function getName(index: number, names: Names, fmt: Format): string { * @param rightPrecedence The precedence of the closest operator to the left of * this subexpression that could affect its bracketing. */ -function render( +function render( e: CompactExpr, names: Names, - fmt: Format, + fmt: Format, leftPrecedence: number, rightPrecedence: number, -): string { +): T { switch (e.head) { case 'variable': { return getName(e.index, names, fmt); @@ -390,7 +497,7 @@ function render( return fmt.verb(e.name, args, event, world); } case 'lambda': { - const symbol = fmt.quantifierSymbols.lambda; + const symbol = fmt.symbolForQuantifier('lambda'); const p = 2; const bracket = rightPrecedence > p; const innerNames = addName(e.type[0], names); @@ -403,7 +510,7 @@ function render( bracket ? 0 : rightPrecedence, ); - let content: string; + let content: T; if (e.restriction === undefined) { content = fmt.quantifier(symbol, name, body); } else { @@ -442,7 +549,7 @@ function render( return bracket ? fmt.bracket(content) : content; } case 'infix': { - const symbol = fmt.infixSymbols[e.name]; + const symbol = fmt.symbolForInfix(e.name); const p = infixPrecedence[e.name]; const associative = infixAssociativity[e.name]; const bracket = @@ -461,7 +568,7 @@ function render( return bracket ? fmt.bracket(content) : content; } case 'polarizer': { - const symbol = fmt.polarizerSymbols[e.name]; + const symbol = fmt.symbolForPolarizer(e.name); const p = 12; const bracket = rightPrecedence > p; const body = render(e.body, names, fmt, p, bracket ? 0 : rightPrecedence); @@ -469,7 +576,7 @@ function render( return bracket ? fmt.bracket(content) : content; } case 'quantifier': { - const symbol = fmt.quantifierSymbols[e.name]; + const symbol = fmt.symbolForQuantifier(e.name); const p = 2; const bracket = rightPrecedence > p; const innerNames = addName(e.body.context[0], names); @@ -482,7 +589,7 @@ function render( bracket ? 0 : rightPrecedence, ); - let content: string; + let content: T; if (e.restriction === undefined) { content = fmt.quantifier(symbol, name, body); } else { @@ -499,16 +606,16 @@ function render( return bracket ? fmt.bracket(content) : content; } case 'constant': { - return fmt.constantSymbols[e.name]; + return fmt.symbolForConstant(e.name); } case 'quote': { return fmt.quote(e.text); } case 'event_compound': { - const symbol = fmt.quantifierSymbols['some']; + const symbol = fmt.symbolForQuantifier('some'); const p = 2; const bracket = rightPrecedence > p; - let body: string | undefined; + let body: T | undefined; const innerNames = addName('v', names); const eventName = getName(0, innerNames, fmt); if (e.body) { @@ -522,9 +629,10 @@ function render( } const world = render(e.world, innerNames, fmt, 0, 0); if (e.aspect.head !== 'infix') throw new Impossible('Non-infix aspect'); - const aspect = - fmt.infixSymbols[e.aspect.name] + - render(e.aspect.right, innerNames, fmt, 0, 0); + const aspect = fmt.aspect( + fmt.symbolForInfix(e.aspect.name), + render(e.aspect.right, innerNames, fmt, 0, 0), + ); const agent = e.agent ? render(e.agent, innerNames, fmt, 0, 0) : undefined; @@ -545,15 +653,15 @@ function render( } } -const renderCache = new Map>(); +const renderCache = new Map, WeakMap>(); -function renderFull(e: CompactExpr, fmt: Format): string { +function renderFull(e: CompactExpr, fmt: Format): T { let cache = renderCache.get(fmt); if (cache === undefined) { cache = new WeakMap(); renderCache.set(fmt, cache); } - const cachedResult = cache.get(e); + const cachedResult = cache.get(e) as T; if (cachedResult !== undefined) return cachedResult; let names = noNames; @@ -578,6 +686,33 @@ export function toLatex(e: CompactExpr): string { return renderFull(e, latex); } +export function toJson(e: CompactExpr): JsonExpr { + return renderFull(e, json) as JsonExpr; +} + +// A replacement for JSON.stringify that uses less linebreaks, à la Haskell coding style. +export function jsonStringifyCompact(expr: any): string { + if (!(typeof expr === 'object')) return JSON.stringify(expr); + + const isArray = Array.isArray(expr); + const [opening, closing] = ['{}', '[]'][+isArray]; + + const entries = Object.entries(expr); + if (!entries.length) return `${opening} ${closing}`; + + return entries + .map(([key, value], index, { length }) => { + const maybeKey = isArray ? '' : `${JSON.stringify(key)}: `; + const valueStringified = jsonStringifyCompact(value) + .split('\n') + .join('\n' + ' '.repeat(isArray ? 4 : 2 + maybeKey.length)); + const maybeOpening = index === 0 ? `${opening} ` : ''; + const commaOrClosing = index === length - 1 ? ` ${closing}` : ','; + return maybeOpening + maybeKey + valueStringified + commaOrClosing; + }) + .join('\n'); +} + export function typeToPlainText(t: ExprType): string { return typeof t === 'string' ? t From 72e188955781866c4c8c63334692a41fb3e1f322 Mon Sep 17 00:00:00 2001 From: uakci Date: Mon, 13 May 2024 20:19:03 +0200 Subject: [PATCH 3/3] sema/render/json: unwrap quantifier-bound names --- src/semantics/render.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/semantics/render.ts b/src/semantics/render.ts index e7e8555..a3f3c86 100644 --- a/src/semantics/render.ts +++ b/src/semantics/render.ts @@ -391,7 +391,7 @@ const json: Format = { symbolForQuantifier: quantifier => quantifier, quantifier: (symbol, name, body) => ({ - [symbol as Quantifier]: name as string, + [symbol as Quantifier]: (name as { variable: string }).variable, body: body as JsonExpr, }) as JsonQuantifierExpr, restrictedQuantifier: (symbol, name, restriction, body) =>