diff --git a/.eslintrc.json b/.eslintrc.json index d5194c5..8cf1b02 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -32,6 +32,7 @@ "functional/immutable-data": "off", "functional/prefer-immutable-types": "off", "functional/no-throw-statements": "off", + "functional/no-mixed-types": "off", "functional/functional-parameters": "off", "functional/prefer-type-literal": "off", "no-constant-condition": "off", diff --git a/src/graph.ts b/src/graph.ts index 9f92ce9..cbf277e 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -1 +1 @@ -export { Graph, Triple, Instance, Edge, Attribute } from './lib/graph'; +export { Graph, Instance, Edge, Attribute } from './lib/graph'; diff --git a/src/index.ts b/src/index.ts index 70e0190..b846571 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,9 @@ -export { parse, iterparse, parseTriples } from './lib/_parse'; -export { format, formatTriples } from './lib/_format'; +export { parse, iterparse, parseTriples } from './lib/parse'; +export { format, formatTriples } from './lib/format'; export { interpret, configure } from './lib/layout'; export { Tree } from './lib/tree'; -export { Graph, Triple } from './lib/graph'; +export { Graph } from './lib/graph'; +export { Triple } from './lib/types'; export { PENMANCodec, decode, diff --git a/src/lib/_lexer.ts b/src/lib/_lexer.ts index 978da05..8445605 100644 --- a/src/lib/_lexer.ts +++ b/src/lib/_lexer.ts @@ -120,6 +120,9 @@ export class TokenIterator { this._next = null; } this._last = current; + if (current == null) { + throw new Error('Unexpected end of input'); + } return current; } @@ -155,7 +158,7 @@ export class TokenIterator { } error(message: string, token?: Token): DecodeError { - let line: string | null = null; + let line: string | undefined; let lineno: number; let offset: number; if (token == null) { @@ -226,7 +229,7 @@ const _lex = function* ( `capturing group:\n${regex.source}`, ); } - const token: Token = [typ, val, i, m.index, line]; + const token: Token = [typ, val, i, m.index ?? 0, line]; debug(`${token}`); yield token; } diff --git a/src/lib/codec.ts b/src/lib/codec.ts index 76988fa..820651b 100644 --- a/src/lib/codec.ts +++ b/src/lib/codec.ts @@ -2,13 +2,13 @@ * Serialization of PENMAN graphs. */ -import { format, formatTriples } from './_format'; -import { iterparse, parse, parseTriples } from './_parse'; +import { format, formatTriples } from './format'; import { Graph } from './graph'; import * as layout from './layout'; import { Model } from './model'; +import { iterparse, parse, parseTriples } from './parse'; import { Tree } from './tree'; -import { BasicTriple, Node, Variable } from './types'; +import { Node, Triple, Variable } from './types'; // "Utility" types; not Penman-specific @@ -18,11 +18,8 @@ import { BasicTriple, Node, Variable } from './types'; export class PENMANCodec { model: Model; - constructor(model: Model | null = null) { - if (model === null) { - model = new Model(); - } - this.model = model; + constructor(model?: Model) { + this.model = model ?? new Model(); } /** @@ -31,7 +28,7 @@ export class PENMANCodec { * @param s - A string containing a single PENMAN-serialized graph. * @returns The `Graph` object described by `s`. * @example - * import { PENMANCodec } from 'penman-js/codec'; + * import { PENMANCodec } from 'penman-js'; * * const codec = new PENMANCodec(); * const graph = codec.decode('(b / bark-01 :ARG0 (d / dog))'); @@ -40,7 +37,7 @@ export class PENMANCodec { */ decode(s: string): Graph { const tree = parse(s); - return layout.interpret(tree, this.model); + return layout.interpret(tree, { model: this.model }); } /** @@ -51,7 +48,7 @@ export class PENMANCodec { */ *iterdecode(lines: string | string[]): IterableIterator { for (const tree of iterparse(lines)) { - yield layout.interpret(tree, this.model); + yield layout.interpret(tree, { model: this.model }); } } @@ -78,7 +75,7 @@ export class PENMANCodec { /** * Parse a triple conjunction from *s*. */ - parseTriples(s: string): BasicTriple[] { + parseTriples(s: string): Triple[] { return parseTriples(s); } @@ -104,7 +101,7 @@ export class PENMANCodec { indent: number | null | undefined = -1, compact = false, ): string { - const tree = layout.configure(g, top, this.model); + const tree = layout.configure(g, { top, model: this.model }); return this.format(tree, indent, compact); } @@ -137,7 +134,7 @@ export class PENMANCodec { * // Expected output: * // 'instance(a, alpha) ^\\nARG0(a, b) ^\\ninstance(b, beta)' */ - formatTriples(triples: BasicTriple[], indent = true): string { + formatTriples(triples: Triple[], indent = true): string { return formatTriples(triples, indent); } } diff --git a/src/lib/constant.ts b/src/lib/constant.ts index 2b507cd..2d45d9e 100644 --- a/src/lib/constant.ts +++ b/src/lib/constant.ts @@ -83,7 +83,7 @@ export const type = (constant_string: string | null | undefined): Type => { export const evaluate = ( constantString: string | null | undefined, ): Constant => { - let value: Constant = constantString; + let value: Constant = constantString ?? null; if (constantString == null || constantString === '') { value = null; } else { diff --git a/src/lib/epigraph.ts b/src/lib/epigraph.ts index a150cc0..c44427d 100644 --- a/src/lib/epigraph.ts +++ b/src/lib/epigraph.ts @@ -1,6 +1,6 @@ /** Base classes for epigraphical markers. */ -import { BasicTriple } from './types'; +import { Triple } from './types'; import { ArrayKeysMap } from './utils'; export class Epidatum { @@ -16,4 +16,4 @@ export class Epidatum { export type Epidata = Epidatum[]; -export class EpidataMap extends ArrayKeysMap {} +export class EpidataMap extends ArrayKeysMap {} diff --git a/src/lib/exceptions.ts b/src/lib/exceptions.ts index d8b933e..3b228fc 100644 --- a/src/lib/exceptions.ts +++ b/src/lib/exceptions.ts @@ -27,25 +27,14 @@ export class LayoutError extends PenmanError {} * @noInheritDoc */ export class DecodeError extends PenmanError { - message: string; - filename: string; - lineno: number; - offset: number; - text: string; - constructor( - message: string = null, - filename: string = null, - lineno: number = null, - offset: number = null, - text: string = null, + message: string, + public filename?: string, + public lineno?: number, + public offset?: number, + public text?: string, ) { super(message); - this.message = message; - this.filename = filename; - this.lineno = lineno; - this.offset = offset; - this.text = text; } toString(): string { diff --git a/src/lib/format.spec.ts b/src/lib/format.spec.ts index c1008e9..41f6e01 100644 --- a/src/lib/format.spec.ts +++ b/src/lib/format.spec.ts @@ -1,7 +1,7 @@ import test from 'ava'; -import { format, formatTriples } from './_format'; -import { BasicTriple, Node } from './types'; +import { format, formatTriples } from './format'; +import { Node, Triple } from './types'; test('format', (t) => { const input: Node = [ @@ -16,7 +16,7 @@ test('format', (t) => { }); test('format_triples', (t) => { - const triples: BasicTriple[] = [ + const triples: Triple[] = [ ['b', 'instance', 'bark-01'], ['b', 'ARG0', 'd'], ['d', 'instance', 'dog'], diff --git a/src/lib/_format.ts b/src/lib/format.ts similarity index 94% rename from src/lib/_format.ts rename to src/lib/format.ts index d7181fa..ccfb395 100644 --- a/src/lib/_format.ts +++ b/src/lib/format.ts @@ -1,5 +1,5 @@ import { isAtomic, Tree } from './tree'; -import { BasicTriple, Branch, Node } from './types'; +import { Branch, Node, Triple } from './types'; import { lstrip } from './utils'; /** @@ -51,10 +51,7 @@ export const format = ( * // ARG0(b, d) ^ * // instance(d, dog) */ -export const formatTriples = ( - triples: BasicTriple[], - indent = true, -): string => { +export const formatTriples = (triples: Triple[], indent = true): string => { const delim = indent ? ' ^\n' : ' ^ '; // need to remove initial : on roles for triples const conjunction = triples.map( @@ -76,7 +73,7 @@ const _formatNode = ( if (!variable) { return '()'; // empty node } - if (!edges.length) { + if (!edges?.length) { return `(${variable})`; // var-only node } // determine appropriate joiner based on value of indent @@ -118,7 +115,7 @@ const _formatNode = ( */ const _formatEdge = ( edge: Branch, - indent: number | null, + indent: number | null | undefined, column: number, vars: Set, ): string => { diff --git a/src/lib/graph.ts b/src/lib/graph.ts index 84a4dd5..dcf7029 100644 --- a/src/lib/graph.ts +++ b/src/lib/graph.ts @@ -8,23 +8,11 @@ import isEqual from 'lodash.isequal'; import { EpidataMap } from './epigraph'; import { GraphError } from './exceptions'; -import type { - BasicTriple, - Constant, - Role, - Target, - Triples, - Variable, -} from './types'; +import type { Constant, Role, Triple, Triples, Variable } from './types'; import { defaultdictPlusEqual } from './utils'; export const CONCEPT_ROLE = ':instance'; -/** - * Represents a relation between nodes or between a node and a constant. - */ -export type Triple = [source: Variable, role: Role, target: Target]; - /** * A relation indicating the concept of a node. */ @@ -77,7 +65,7 @@ export class Graph { */ constructor( public triples: Triples = [], - top: Variable = null, + top: Variable | null = null, public epidata: EpidataMap = new EpidataMap(), public metadata: Record = {}, ) { @@ -189,10 +177,10 @@ export class Graph { } const possible_sources = this.triples.map((t) => t[0]); const possible_targets = this.triples.map((t) => t[2]); - const possible_variables = new Set( + const possibleVariables = new Set( possible_targets.concat(possible_sources), ); - if (!possible_variables.has(this._top)) { + if (!possibleVariables.has(this._top)) { this._top = null; } return this; @@ -271,7 +259,7 @@ export class Graph { source: Variable | null = null, role: Role | null = null, target: Constant | null = null, - ): BasicTriple[] { + ): Triple[] { // TODO: check for undefined OR null if (source == null && role == null && target == null) { return this.triples.slice(); diff --git a/src/lib/layout.spec.ts b/src/lib/layout.spec.ts index 6e8fc60..d0e34c4 100644 --- a/src/lib/layout.spec.ts +++ b/src/lib/layout.spec.ts @@ -41,7 +41,7 @@ test('interpret', (t) => { ), ); t.true( - interpret(t2, amrModel()).equals( + interpret(t2, { model: amrModel() }).equals( new Graph( [ ['a', ':instance', 'A'], @@ -62,7 +62,7 @@ test('rearrange', (t) => { :ARG1 (d / delta)) :ARG0-of d :ARG1 (e / epsilon))`); - rearrange(t1, model.originalOrder); + rearrange(t1, { key: model.originalOrder }); t.is( codec.format(t1), `(a / alpha @@ -73,7 +73,7 @@ test('rearrange', (t) => { :ARG1 (e / epsilon))`, ); - rearrange(t1, model.canonicalOrder.bind(model)); + rearrange(t1, { key: model.canonicalOrder.bind(model) }); t.is( codec.format(t1), `(a / alpha @@ -87,7 +87,7 @@ test('rearrange', (t) => { test('configure', (t) => { const g = codec.decode('(a / A)'); t.deepEqual(configure(g), new Tree(['a', [['/', 'A']]])); - t.throws(() => configure(g, 'A'), { instanceOf: LayoutError }); + t.throws(() => configure(g, { top: 'A' }), { instanceOf: LayoutError }); const g1 = codec.decode('(a / A :consist-of (b / B))'); t.deepEqual( @@ -101,7 +101,7 @@ test('configure', (t) => { ]), ); t.deepEqual( - configure(g1, 'b'), + configure(g1, { top: 'b' }), new Tree([ 'b', [ @@ -114,7 +114,7 @@ test('configure', (t) => { const amrCodec = new PENMANCodec(amrModel()); const g2 = amrCodec.decode('(a / A :consist-of (b / B))'); t.deepEqual( - configure(g2, undefined, amrModel()), + configure(g2, { model: amrModel() }), new Tree([ 'a', [ @@ -124,7 +124,7 @@ test('configure', (t) => { ]), ); t.deepEqual( - configure(g2, 'b', amrModel()), + configure(g2, { top: 'b', model: amrModel() }), new Tree([ 'b', [ @@ -222,7 +222,7 @@ test('reconfigure', (t) => { // canonical order reconfiguration can also shift things like // inverted arguments t.deepEqual( - reconfigure(g, undefined, undefined, model.canonicalOrder.bind(model)), + reconfigure(g, { key: model.canonicalOrder.bind(model) }), new Tree([ 'a', [ @@ -343,7 +343,7 @@ test('issue 92', (t) => { ]), ); t.deepEqual( - configure(g, 'b'), + configure(g, { top: 'b' }), new Tree([ 'b', [ diff --git a/src/lib/layout.ts b/src/lib/layout.ts index 88511b0..5a031fe 100644 --- a/src/lib/layout.ts +++ b/src/lib/layout.ts @@ -68,15 +68,7 @@ import { debug, log, warn } from './logger'; import { Model } from './model'; import { Alignment, RoleAlignment } from './surface'; import { isAtomic, Tree } from './tree'; -import { - BasicTriple, - Branch, - Constant, - Node, - Role, - Triples, - Variable, -} from './types'; +import { Branch, Node, Role, Triple, Triples, Variable } from './types'; const _default_model = new Model(); @@ -111,6 +103,10 @@ export class Pop extends LayoutMarker { /** A singleton instance of `Pop`. */ export const POP = new Pop(); +export interface InterpretOptions { + model?: Model; +} + /** * Interpret tree `t` as a graph using `model`. * @@ -119,8 +115,10 @@ export const POP = new Pop(); * which edges are inverted and how to deinvert them. If `model` is * not provided, the default model will be used. * + * The `options` param consists of an object with a single `model` property. + * * @param t - The `Tree` object to interpret. - * @param model - The `Model` used to interpret `t`. + * @param options - An object with an optional `model` property, used to interpret `t`. * @returns The interpreted `Graph` object. * @example * import { Tree } from 'penman-js/tree'; @@ -141,13 +139,8 @@ export const POP = new Pop(); * // ['b', ':ARG0', 'd'] * // ['d', ':instance', 'dog'] */ -export function interpret( - t: Tree | null, - model: Model = _default_model, -): Graph { - if (t == null) { - model = _default_model; - } +export function interpret(t: Tree, options: InterpretOptions = {}): Graph { + const { model = _default_model } = options; const variables = new Set(t.nodes().map((node) => node[0])); const [top, triples, epidata] = _interpretNode(t.node, variables, model); const epimap = new EpidataMap(); @@ -167,12 +160,12 @@ const _interpretNode = ( t: Node, variables: Set, model: Model, -): [string, Triples, [BasicTriple, Epidata][]] => { +): [string, Triples, [Triple, Epidata][]] => { let hasConcept = false; const triples: Triples = []; - const epidata: [BasicTriple, Epidata][] = []; + const epidata: [Triple, Epidata][] = []; const [variable, edges] = t; - for (const edge of edges) { + for (const edge of edges ?? []) { let role = edge[0]; const target = edge[1]; const epis: Epidatum[] = []; @@ -187,7 +180,7 @@ const _interpretNode = ( if (isAtomic(target)) { const [target_, target_epis] = _processAtomic(target); epis.push(...target_epis); - let triple: BasicTriple = [variable, role, target_]; + let triple: Triple = [variable, role, target_]; if (model.isRoleInverted(role)) { if (variables.has(target_)) { triple = model.invert(triple); @@ -215,7 +208,7 @@ const _interpretNode = ( } if (!hasConcept) { - const instance: BasicTriple = [variable, CONCEPT_ROLE, null]; + const instance: Triple = [variable, CONCEPT_ROLE, null]; triples.unshift(instance); epidata.push([instance, []]); } @@ -256,6 +249,11 @@ const _processAtomic = (target: string): [string, Epidatum[]] => { return [target, epis]; }; +export interface ConfigureOptions { + top?: Variable; + model?: Model; +} + /** * Create a tree from a graph by making as few decisions as possible. * @@ -269,10 +267,14 @@ const _processAtomic = (target: string): [string, Epidatum[]] => { * deterministic, but may result in a tree different than the one * expected. * + * `options` consists of an object with optional `top` and `model` properties. + * - `top` is the variable to use as the top of the graph; if `null`, the top of `g` will be used. + * - `model` is the `Model` used to configure the tree. + * * @param g - The `Graph` object to configure. - * @param top - The variable to use as the top of the graph; if `null`, - * the top of `g` will be used. - * @param model - The `Model` used to configure the tree. + * @param options - An object with optional `top` and `model` properties. + * - `top` is the variable to use as the top of the graph; if `null`, the top of `g` will be used. + * - `model` is the `Model` used to configure the tree. * @returns The configured `Tree` object. * @example * import { Graph } from 'penman-js/graph'; @@ -288,14 +290,8 @@ const _processAtomic = (target: string): [string, Epidatum[]] => { * console.log(t); * // Tree('b', [['/', 'bark-01'], [':ARG0', new Tree('d', [['/', 'dog']])]]) */ -export function configure( - g: Graph, - top: Variable = null, - model: Model = null, -): Tree { - if (model == null) { - model = _default_model; - } +export function configure(g: Graph, options: ConfigureOptions = {}): Tree { + const { top = g.top, model = _default_model } = options; const configRes = _configure(g, top, model); const node = configRes[0]; let data = configRes[1]; @@ -306,7 +302,7 @@ export function configure( data.pop(); } // if any data remain, the graph was not properly annotated for a tree - const skipped = []; + const skipped: ([Triple, boolean, Epidata] | Pop)[] = []; while (data.length > 0) { const next = _findNext(data, nodemap); const _skipped = next[0]; @@ -320,7 +316,7 @@ export function configure( } const surprising = _configureNode(variable, data, nodemap, model)[1]; if (data.length === dataCount && surprising) { - skipped.unshift(data.pop()); + skipped.unshift(data.pop()!); } else if (data.length >= dataCount) { throw new LayoutError('unknown configuration error'); } else { @@ -346,18 +342,18 @@ export function configure( */ function _configure( g: Graph, - top: Variable, + top: Variable | null, model: Model, -): [Node, ([BasicTriple, boolean, Epidata] | Pop)[], _Nodemap] { +): [Node, ([Triple, boolean, Epidata] | Pop)[], _Nodemap] { if (g.triples.length === 0) { - return [[g.top, []], [], {}]; + return [[g.top as string, []], [], {}]; } const nodemap: _Nodemap = {}; for (const variable of g.variables()) { nodemap[variable] = null; } if (top == null) { - top = g.top; + throw new LayoutError(`top is not a variable: ${top}`); } if (!(top in nodemap)) { throw new LayoutError(`top is not a variable: ${top}`); @@ -373,7 +369,7 @@ function _configure( * Also perform some basic validation. */ function _preconfigure(g: Graph, model: Model) { - const data: ([BasicTriple, boolean, Epidata] | Pop)[] = []; + const data: ([Triple, boolean, Epidata] | Pop)[] = []; const epidata = g.epidata; const pushed = new Set(); @@ -422,18 +418,18 @@ function _preconfigure(g: Graph, model: Model) { */ function _configureNode( variable: Variable, - data: ([BasicTriple, boolean, Epidata] | Pop)[], - nodemap: { [key: Constant]: Node | null }, + data: ([Triple, boolean, Epidata] | Pop)[], + nodemap: _Nodemap, model: Model, ): [Node, boolean] { - const node = nodemap[variable]; - const edges = node[1]; + const node = nodemap[variable]!; + const edges = node[1] ?? []; // Something is 'surprising' when a triple doesn't predictably fit // given the current state let surprising = false; while (data.length) { - const datum = data.pop(); + const datum = data.pop()!; if (datum instanceof Pop) { break; } @@ -482,14 +478,14 @@ function _configureNode( * Find the next node context; establish if necessary. */ function _findNext( - data: ([BasicTriple, boolean, Epidata] | Pop)[], - nodemap: { [key: Constant]: Node | null }, + data: ([Triple, boolean, Epidata] | Pop)[], + nodemap: _Nodemap, ): [ - ([BasicTriple, boolean, Epidata] | Pop)[], + ([Triple, boolean, Epidata] | Pop)[], Variable, - ([BasicTriple, boolean, Epidata] | Pop)[], + ([Triple, boolean, Epidata] | Pop)[], ] { - let variable = null; + let variable: string | number | null = null; let pivot = data.length; for (let i = data.length - 1; i >= 0; i--) { const datum = data[i]; @@ -503,6 +499,7 @@ function _findNext( variable = source; break; } else if ( + target != null && target in nodemap && _getOrEstablishSite(target as string, nodemap) ) { @@ -510,18 +507,15 @@ function _findNext( break; } } - return [data.slice(pivot), variable, data.slice(0, pivot)]; + return [data.slice(pivot), variable as string, data.slice(0, pivot)]; } /** * Turn a variable target into a node context. */ -function _getOrEstablishSite( - variable: Variable, - nodemap: { [key: Constant]: Node | null }, -): boolean { +function _getOrEstablishSite(variable: Variable, nodemap: _Nodemap): boolean { // first check if the var is available at all if (nodemap[variable] != null) { - const [_var, edges] = nodemap[variable]; + const [_var, edges = []] = nodemap[variable] as Node; // if the mapped node's var doesn't match it can be established if (variable !== _var) { const node: Node = [variable, []]; @@ -573,16 +567,26 @@ function _processEpigraph(node: any): void { } } +export interface ReconfigureOptions { + top?: Variable; + model?: Model; + key?: (role: Role) => any; +} + /** * Create a tree from a graph after any discarding layout markers. + * + * `options` consists of an object with optional `top`, `model`, and `key` properties. * If `key` is provided, triples are sorted according to the key. + * + * @param graph - The `Graph` object to reconfigure. + * @param options - An object with optional `top`, `model`, and `key` properties. */ export function reconfigure( graph: Graph, - top: Variable = null, - model: Model = null, - key: (role: Role) => any = null, + options: ReconfigureOptions = {}, ): Tree { + const { top, model, key } = options; const p = cloneDeep(graph); for (const entry of p.epidata.entries()) { const epilist = entry[1]; @@ -596,7 +600,12 @@ export function reconfigure( if (key != null) { p.triples = sortBy(p.triples, (x) => key(x[1])); } - return configure(p, top, model); + return configure(p, { top, model }); +} + +export interface RearrangeOptions { + key?: (role: Role) => any; + attributesFirst?: boolean; } /** @@ -606,14 +615,14 @@ export function reconfigure( * those lists in-place using the `key` function, which accepts a role and * returns some sortable criterion. * - * If the `attributesFirst` argument is `true`, attribute branches will - * appear before any edges. + * `options` consists of an object with optional `key` and `attributesFirst` properties. + * - If the `attributesFirst` argument is `true`, attribute branches will appear before any edges. + * - `key` is a function used for sorting branches. * * Instance branches (`/`) always appear before any other branches. * * @param t - The tree to rearrange. - * @param key - The function used for sorting branches. - * @param attributesFirst - If `true`, attribute branches appear before edges. + * @param options - An object with optional `key` and `attributesFirst` properties. * @example * import { rearrange } from 'penman-js/layout'; * import { Model } from 'penman-js/model'; @@ -630,11 +639,8 @@ export function reconfigure( * // :ARG0 (d / dog) * // :ARG1 (c / cat)) */ -export function rearrange( - t: Tree, - key: (role: Role) => any = null, - attributesFirst = false, -): void { +export function rearrange(t: Tree, options: RearrangeOptions = {}): void { + const { key = null, attributesFirst = false } = options; let variables = new Set(); if (attributesFirst) { variables = new Set(t.nodes().map((n) => n[0])); @@ -651,8 +657,8 @@ export function rearrange( } const _rearrange = (node: Node, key: (branch: Branch) => any) => { - const [, branches] = node; - let first = []; + const [, branches = []] = node; + let first: Branch[] = []; let rest = branches.slice(); if (branches && branches[0][0] === '/') { first = branches.slice(0, 1); @@ -680,10 +686,7 @@ const _rearrange = (node: Node, key: (branch: Branch) => any) => { * console.log(getPushedVariable(g, ['a', ':instance', 'alpha'])); // Outputs: null * console.log(getPushedVariable(g, ['a', ':ARG0', 'b'])); // Outputs: 'b' */ -export function getPushedVariable( - g: Graph, - triple: BasicTriple, -): Variable | null { +export function getPushedVariable(g: Graph, triple: Triple): Variable | null { for (const epi of g.epidata.get(triple) ?? []) { if (epi instanceof Push) { return epi.variable; @@ -708,7 +711,7 @@ export function getPushedVariable( * @param triple - The triple that does or does not appear inverted. * @returns `true` if `triple` appears inverted in graph `g`. */ -export function appearsInverted(g: Graph, triple: BasicTriple): boolean { +export function appearsInverted(g: Graph, triple: Triple): boolean { const variables = g.variables(); if (triple[1] === CONCEPT_ROLE || !variables.has(triple[2] as string)) { // attributes and instance triples should never be inverted @@ -774,7 +777,7 @@ export function nodeContexts(g: Graph): Array { eligible.push(triple[2] as Variable); } - if (!eligible.includes(stack[stack.length - 1])) { + if (!eligible.includes(stack[stack.length - 1] as string)) { break; } else { contexts[i] = stack[stack.length - 1]; diff --git a/src/lib/model.spec.ts b/src/lib/model.spec.ts index 1eeacb2..e431708 100644 --- a/src/lib/model.spec.ts +++ b/src/lib/model.spec.ts @@ -3,7 +3,7 @@ import test from 'ava'; import { miniAmr } from './fixtures'; import { Graph } from './graph'; import { Model } from './model'; -import { BasicTriple } from './types'; +import { Triple } from './types'; test('init', (t) => { const m = new Model(); @@ -279,10 +279,10 @@ test('isConceptDereifiable', (t) => { test('dereify', (t) => { // (a :ARG1-of (_ / age-01 :ARG2 b)) -> (a :age b) - const t1: BasicTriple = ['_', ':instance', 'have-mod-91']; - const t1b: BasicTriple = ['_', ':instance', 'chase-01']; - const t2: BasicTriple = ['_', ':ARG1', 'a']; - const t3: BasicTriple = ['_', ':ARG2', 'b']; + const t1: Triple = ['_', ':instance', 'have-mod-91']; + const t1b: Triple = ['_', ':instance', 'chase-01']; + const t2: Triple = ['_', ':ARG1', 'a']; + const t3: Triple = ['_', ':ARG2', 'b']; const m1 = new Model(); t.throws(() => (m1 as any).dereify(t1)); t.throws(() => (m1 as any).dereify(t1, t2)); diff --git a/src/lib/model.ts b/src/lib/model.ts index 3887365..78a0153 100644 --- a/src/lib/model.ts +++ b/src/lib/model.ts @@ -4,12 +4,12 @@ import { ModelError } from './exceptions'; import { CONCEPT_ROLE, Graph } from './graph'; -import { BasicTriple, Constant, Role, Target, Variable } from './types'; +import { Constant, Role, Target, Triple, Variable } from './types'; export type _ReificationSpec = [Role, Constant, Role, Role]; type _Reified = [Constant, Role, Role]; type _Dereified = [Role, Role, Role]; -type _Reification = [BasicTriple, BasicTriple, BasicTriple]; +type _Reification = [Triple, Triple, Triple]; /** * Represents a semantic model for Penman graphs. @@ -17,8 +17,8 @@ type _Reification = [BasicTriple, BasicTriple, BasicTriple]; * The model defines elements such as valid roles and transformations. */ export class Model { - reifications: { [key: Role]: Array<_Reified> }; - dereifications: { [key: Constant]: Array<_Dereified> }; + reifications: Map>; + dereifications: Map>; topVariable: Variable; topRole: Role; conceptRole: Role; @@ -61,18 +61,18 @@ export class Model { } this.normalizations = normalizations || {}; - const reifs: { [key: Role]: Array<_Reified> } = {}; - const deifs: { [key: Constant]: Array<_Dereified> } = {}; + const reifs: Map> = new Map(); + const deifs: Map> = new Map(); if (reifications) { for (const [role, concept, source, target] of reifications) { if (reifs[role] === undefined) { reifs[role] = []; } reifs[role].push([concept, source, target]); - if (deifs[concept] === undefined) { - deifs[concept] = []; + if (!deifs.has(concept)) { + deifs.set(concept, []); } - deifs[concept].push([role, source, target]); + deifs.get(concept)!.push([role, source, target]); } } this.reifications = reifs; @@ -158,7 +158,7 @@ export class Model { * @param triple - The triple to invert. * @returns The inverted or deinverted triple. */ - invert(triple: BasicTriple): BasicTriple { + invert(triple: Triple): Triple { const [source, role, target] = triple; const inverse = this.invertRole(role); // casting is just for the benefit of the type checker; it does @@ -178,7 +178,7 @@ export class Model { * @param triple - The triple to de-invert if necessary. * @returns The de-inverted triple, or the original triple if it wasn't inverted. */ - deinvert(triple: BasicTriple): BasicTriple { + deinvert(triple: Triple): Triple { if (this.isRoleInverted(triple[1])) { triple = this.invert(triple); } @@ -232,7 +232,7 @@ export class Model { * @param triple - The triple to be canonicalized. * @returns The canonicalized triple. */ - canonicalize(triple: BasicTriple): BasicTriple { + canonicalize(triple: Triple): Triple { const [source, role, target] = triple; const canonical = this.canonicalizeRole(role); return [source, canonical, target]; @@ -263,7 +263,7 @@ export class Model { * @throws {ModelError} - If the role of `triple` does not have a defined reification. */ - reify(triple: BasicTriple, variables?: Set): _Reification { + reify(triple: Triple, variables?: Set): _Reification { const [source, role, target] = triple; if (!(role in this.reifications)) { throw new ModelError(`'${role}' cannot be reified`); @@ -287,10 +287,10 @@ export class Model { } /** - * Return ``True`` if *concept* can be dereified. + * Return `true` if `concept` can be dereified. */ isConceptDereifiable(concept: Target): boolean { - return concept in this.dereifications; + return this.dereifications.has(concept); } /** @@ -311,10 +311,10 @@ export class Model { * @throws {ValueError} - If `instanceTriple` is not valid or if any triple has a different source. */ dereify( - instanceTriple: BasicTriple, - sourceTriple: BasicTriple, - targetTriple: BasicTriple, - ): BasicTriple { + instanceTriple: Triple, + sourceTriple: Triple, + targetTriple: Triple, + ): Triple { if (instanceTriple[1] !== CONCEPT_ROLE) { throw new Error('second argument is not an instance triple'); } @@ -329,10 +329,10 @@ export class Model { const sourceRole = sourceTriple[1]; const targetRole = targetTriple[1]; - if (!(concept in this.dereifications)) { + if (!this.dereifications.has(concept)) { throw new ModelError(`${concept} cannot be dereified`); } - for (const [role, source, target] of this.dereifications[concept]) { + for (const [role, source, target] of this.dereifications.get(concept)!) { if (source === sourceRole && target === targetRole) { return [sourceTriple[2] as Variable, role, targetTriple[2]]; } else if (target === sourceRole && source === targetRole) { @@ -413,7 +413,7 @@ export class Model { if (graph.triples.length === 0) { err[''] = ['graph is empty']; } else { - const g: { [key: string]: BasicTriple[] } = {}; + const g: { [key: string]: Triple[] } = {}; for (const triple of graph.triples) { const [variable, role] = triple; if (!this.hasRole(role)) { @@ -427,10 +427,10 @@ export class Model { if (!graph.top) { err[''] = ['top is not set']; } - if (!(graph.top in g)) { + if (!((graph.top as string) in g)) { err[''] = ['top is not a variable in the graph']; } - const reachable = _dfs(g, graph.top); + const reachable = _dfs(g, graph.top as string); const unreachable = Object.keys(g).filter((k) => !reachable.has(k)); for (const uvar of unreachable) { for (const triple of g[uvar]) { @@ -443,7 +443,7 @@ export class Model { } } -function _dfs(g: { [key: string]: BasicTriple[] }, top: string): Set { +function _dfs(g: { [key: string]: Triple[] }, top: string): Set { // just keep source and target of edge relations const q: { [key: string]: Set } = {}; for (const [variable, triples] of Object.entries(g)) { @@ -464,7 +464,7 @@ function _dfs(g: { [key: string]: BasicTriple[] }, top: string): Set { const visited = new Set(); const agenda = [top]; while (agenda.length > 0) { - const cur = agenda.pop(); + const cur = agenda.pop()!; if (!visited.has(cur)) { visited.add(cur); for (const t of q[cur]) { diff --git a/src/lib/models/noop.ts b/src/lib/models/noop.ts index 8aef650..b0c3a94 100644 --- a/src/lib/models/noop.ts +++ b/src/lib/models/noop.ts @@ -1,7 +1,7 @@ /** No-op semantic model definition. */ import { Model } from '../model'; -import { BasicTriple } from '../types'; +import { Triple } from '../types'; /** * A no-operation model that mostly leaves things alone. @@ -12,7 +12,7 @@ import { BasicTriple } from '../types'; */ class NoOpModel extends Model { /** Return *triple* (does not deinvert). */ - deinvert(triple: BasicTriple): BasicTriple { + deinvert(triple: Triple): Triple { return triple; } } diff --git a/src/lib/_parse.ts b/src/lib/parse.ts similarity index 96% rename from src/lib/_parse.ts rename to src/lib/parse.ts index 9b18b62..8a735e7 100644 --- a/src/lib/_parse.ts +++ b/src/lib/parse.ts @@ -1,7 +1,7 @@ import { lex, PENMAN_RE, Token, TokenIterator, TRIPLE_RE } from './_lexer'; import { debug, warn } from './logger'; import { Tree } from './tree'; -import { BasicTriple, Branch, Node, Target } from './types'; +import { Branch, Node, Target, Triple } from './types'; /** * Parse a PENMAN-notation string `s` into its tree structure. @@ -66,7 +66,7 @@ export function* iterparse( * } */ -export const parseTriples = (s: string): BasicTriple[] => { +export const parseTriples = (s: string): Triple[] => { const tokens = lex(s, TRIPLE_RE); return _parseTriples(tokens); }; @@ -113,7 +113,7 @@ const _parseNode = (tokens: TokenIterator): Node => { let variable: string | null = null; let concept: string | null = null; - const edges = []; + const edges: Branch[] = []; if (tokens.peek()[0] !== 'RPAREN') { variable = tokens.expect('SYMBOL')[1]; @@ -137,7 +137,7 @@ const _parseNode = (tokens: TokenIterator): Node => { } tokens.expect('RPAREN'); - return [variable, edges]; + return [variable as string, edges]; }; /** @@ -172,9 +172,9 @@ const _parseEdge = (tokens: TokenIterator): Branch => { return [role, target]; }; -const _parseTriples = (tokens: TokenIterator): BasicTriple[] => { +const _parseTriples = (tokens: TokenIterator): Triple[] => { let target: Target; - const triples = []; + const triples: Triple[] = []; let stripCaret = false; while (true) { let role = tokens.expect('SYMBOL')[1]; diff --git a/src/lib/surface.ts b/src/lib/surface.ts index 2725568..518de6a 100644 --- a/src/lib/surface.ts +++ b/src/lib/surface.ts @@ -5,7 +5,7 @@ import { Epidatum } from './epigraph'; import { SurfaceError } from './exceptions'; import { Graph } from './graph'; -import { BasicTriple } from './types'; +import { Triple } from './types'; import { ArrayKeysMap, lstrip } from './utils'; export class AlignmentMarker extends Epidatum { @@ -92,7 +92,7 @@ export class RoleAlignment extends AlignmentMarker { mode = 1; } -type _Alignments = ArrayKeysMap; +type _Alignments = ArrayKeysMap; /** * Return a mapping of triples to alignments in graph `g`. @@ -147,7 +147,7 @@ const _getAlignments = ( g: Graph, alignmentType: typeof AlignmentMarker, ): _Alignments => { - const alns = new ArrayKeysMap(); + const alns = new ArrayKeysMap(); for (const [triple, epidata] of g.epidata) { for (const epidatum of epidata) { if (epidatum instanceof alignmentType) { diff --git a/src/lib/transform.spec.ts b/src/lib/transform.spec.ts index 047a374..c57f986 100644 --- a/src/lib/transform.spec.ts +++ b/src/lib/transform.spec.ts @@ -20,8 +20,9 @@ const makeNorm = (func: (x: any, model: Model) => any, model: Model) => (x: any) => func(x, model); -const makeForm = (func: (x: any, indent?: number) => string) => (x: any) => - func(x, null); +const makeForm = + (func: (x: any, indent?: number | null) => string) => (x: any) => + func(x, null); test('canonicalize_roles_default_codec', (t) => { const parse = defCodec.parse; diff --git a/src/lib/transform.ts b/src/lib/transform.ts index 4f5df69..73bafb7 100644 --- a/src/lib/transform.ts +++ b/src/lib/transform.ts @@ -8,7 +8,7 @@ import { log } from './logger'; import { Model } from './model'; import { Alignment, alignments, RoleAlignment } from './surface'; import { isAtomic, Tree } from './tree'; -import { BasicTriple, Branch, Node, Target, Variable } from './types'; +import { Branch, Node, Target, Triple, Variable } from './types'; import { partition } from './utils'; /** @@ -46,7 +46,7 @@ export const canonicalizeRoles = ( }; const _canonicalizeNode = (node: Node, model: Model): Node => { - const [variable, edges] = node; + const [variable, edges = []] = node; const canonicalEdges: [string, Branch[]][] = []; for (const edge of edges) { const rawRole = edge[0]; @@ -87,7 +87,7 @@ export const reifyEdges = (g: Graph, model: Model | null = null): Graph => { model = new Model(); } const newEpidata = new EpidataMap(g.epidata.entries()); - const newTriples: BasicTriple[] = []; + const newTriples: Triple[] = []; for (const triple of g.triples) { if (model.isRoleReifiable(triple[1])) { const reified = model.reify(triple, vars); @@ -145,11 +145,11 @@ export const dereifyEdges = (g: Graph, model: Model | null = null): Graph => { } const agenda: _Dereification = _dereifyAgenda(g, model); const newEpidata = new EpidataMap(g.epidata.entries()); - const newTriples: BasicTriple[] = []; + const newTriples: Triple[] = []; for (const triple of g.triples) { const variable = triple[0]; if (agenda.has(variable)) { - const [first, dereified, epidata] = agenda.get(variable); + const [first, dereified, epidata] = agenda.get(variable)!; // only insert at the first triple so the dereification // appears in the correct location if (triple === first) { @@ -188,7 +188,7 @@ export const dereifyEdges = (g: Graph, model: Model | null = null): Graph => { export const reifyAttributes = (g: Graph): Graph => { const variables = g.variables(); const newEpidata = new EpidataMap(g.epidata.entries()); - const newTriples: BasicTriple[] = []; + const newTriples: Triple[] = []; let i = 2; for (const triple of g.triples) { const [source, role, target] = triple; @@ -200,8 +200,8 @@ export const reifyAttributes = (g: Graph): Graph => { i += 1; } variables.add(variable); - const roleTriple: BasicTriple = [source, role, variable]; - const nodeTriple: BasicTriple = [variable, CONCEPT_ROLE, target]; + const roleTriple: Triple = [source, role, variable]; + const nodeTriple: Triple = [variable, CONCEPT_ROLE, target]; newTriples.push(roleTriple, nodeTriple); // manage epigraphical markers const oldEpis = newEpidata.has(triple) ? newEpidata.pop(triple) : []; @@ -249,7 +249,7 @@ export const reifyAttributes = (g: Graph): Graph => { * // :ARG0 b)) */ export const indicateBranches = (g: Graph, model: Model): Graph => { - const newTriples: BasicTriple[] = []; + const newTriples: Triple[] = []; for (const t of g.triples) { const push = g.epidata.get(t)?.find((epi) => epi instanceof Push) as Push; if (push != null) { @@ -339,8 +339,8 @@ const _edgeMarkers = (epidata: Epidata): [Epidata, Epidata] => { type _Dereification = Map< Variable, [ - BasicTriple, // inverted triple of reification - BasicTriple, // dereified triple + Triple, // inverted triple of reification + Triple, // dereified triple Epidatum[], // computed epidata ] >; @@ -349,8 +349,8 @@ const _dereifyAgenda = (g: Graph, model: Model): _Dereification => { const alns = alignments(g); const agenda: _Dereification = new Map(); const fixed: Set = new Set([g.top]); - const inst: Map = new Map(); - const other: Map = new Map(); + const inst: Map = new Map(); + const other: Map = new Map(); for (const triple of g.triples) { const [variable, role, tgt] = triple; @@ -374,7 +374,7 @@ const _dereifyAgenda = (g: Graph, model: Model): _Dereification => { ) { // passed initial checks // now figure out which other edge is the first one - let [first, second] = other.get(variable); + let [first, second] = other.get(variable)!; if (getPushedVariable(g, second) === variable) { [first, second] = [second, first]; } diff --git a/src/lib/tree.ts b/src/lib/tree.ts index 7d953dd..3551e88 100644 --- a/src/lib/tree.ts +++ b/src/lib/tree.ts @@ -97,7 +97,7 @@ export class Tree { const varmap: VarMap = {}; const used: Set = new Set(); for (const node of this.nodes()) { - const [variable, branches] = node; + const [variable, branches = []] = node; if (!(variable in varmap)) { const concept = branches.find((branch) => branch[0] === '/')?.[1]; const pre = _defaultVariablePrefix(concept); @@ -120,7 +120,7 @@ export class Tree { } const _format = (node: Node, level: number): string => { - const [variable, branches] = node; + const [variable, branches = []] = node; const next_level = level + 2; const indent = '\n' + ' '.repeat(next_level); const branch_strings = branches.map((branch) => @@ -141,7 +141,7 @@ const _formatBranch = (branch: Branch, level: number): string => { }; const _nodes = (node: Node): Node[] => { - const [variable, branches] = node; + const [variable, branches = []] = node; let ns = variable ? [node] : []; for (const branch of branches) { const target = branch[1]; @@ -154,7 +154,7 @@ const _nodes = (node: Node): Node[] => { }; function* _walk(node: Node, path: number[]) { - const branches = node[1]; + const branches = node[1] ?? []; for (const [i, branch] of branches.entries()) { const curpath = path.concat([i]); yield [curpath, branch]; @@ -201,7 +201,7 @@ function isalpha(c: string): boolean { } const _mapVars = (node: Node, varmap: VarMap): Node => { - const [variable, branches] = node; + const [variable, branches = []] = node; const newbranches: Branch[] = []; for (const branch of branches) { diff --git a/src/lib/types.ts b/src/lib/types.ts index d74a9a8..309b3a9 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -10,5 +10,6 @@ export type Node = [Variable, Branch[]] | [Variable]; // Graph types export type Target = Variable | Constant; -export type BasicTriple = [Variable, Role, Target]; -export type Triples = BasicTriple[]; +/** Represents a relation between nodes or between a node and a constant. */ +export type Triple = [source: Variable, role: Role, target: Target]; +export type Triples = Triple[]; diff --git a/tsconfig.json b/tsconfig.json index 3cdf1da..5d12667 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ /* Strict Type-Checking Options */ // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, - // "strictNullChecks": true /* Enable strict null checks. */, + "strictNullChecks": true /* Enable strict null checks. */, // "strictFunctionTypes": true /* Enable strict checking of function types. */, // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,