You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ui/src/support/page.ts

204 lines
6.6 KiB

import {MathStatement} from './parse'
import * as math from 'mathjs'
import {DepGraph} from 'dependency-graph'
import { v4 as uuidv4 } from 'uuid'
import {EvaluationResult, hasOwnProperty, Maybe, StatementID, VariableName} from './types'
/**
* Wrapper for a page containing multiple interrelated mathematical statements.
*/
export class MathPage {
/** The statements on the page. */
statements: Record<StatementID, MathStatement> = {}
static deserialize(vals: [string, [StatementID, string, number, number][]]) {
const inst = new this(vals[0])
for ( const row of vals[1] ) {
inst.statements[row[0]] = MathStatement.deserialize(row)
}
return inst
}
constructor(
/** Unique page ID. */
public readonly id: string,
) {}
serialize(): [string, [StatementID, string, number, number][]] {
return [
this.id,
Object.values(this.statements).map(x => x.serialize()),
]
}
/** Get all defined statements. */
getStatements(): MathStatement[] {
return Object.values(this.statements)
}
/** Remove a statement from the math page. */
removeStatement(id: StatementID) {
delete this.statements[id]
}
/** Get a statement by ID if it exists. */
getStatement(id: StatementID): Maybe<MathStatement> {
return this.statements[id]
}
/** Add a statement to this page. */
addStatement(statement: MathStatement): this {
this.statements[statement.id] = statement
return this
}
/** Parse the math expression and add it to the page as a statement. */
addRaw(statement: string): StatementID {
const stmt = new MathStatement(uuidv4() as StatementID, statement)
this.addStatement(stmt)
return stmt.id
}
/** Get all symbols referenced by statements on this page. */
symbols(): math.SymbolNode[] {
return Object.values(this.statements)
.map(x => x.symbols())
.reduce((carry, current) => current.concat(carry), [])
}
/** Get all symbols defined on this page. */
defines(): math.SymbolNode[] {
return Object.values(this.statements)
.map(x => x.defines())
.reduce((carry, current) => current.concat(carry), [])
}
/** Get all symbols used on this page. */
uses(): math.SymbolNode[] {
return Object.values(this.statements)
.map(x => x.uses())
.reduce((carry, current) => current.concat(carry), [])
}
/** Get a mapping of symbol names to the statements where they are defined. */
definers(): Record<VariableName, MathStatement> {
const definers: Record<VariableName, MathStatement> = {}
for ( const statement of Object.values(this.statements) ) {
for ( const symbol of statement.defines() ) {
definers[symbol.name as VariableName] = statement
}
}
return definers
}
/** Get the dependency graph of variable declarations between statements on this page. */
dependencies(): DepGraph<MathStatement> {
const graph = new DepGraph<MathStatement>()
const defined: Record<VariableName, MathStatement> = this.definers()
const definedFunctions: Record<VariableName, MathStatement> = {}
for ( const sym of this.functions() ) {
const node = sym.parse() as math.FunctionAssignmentNode
definedFunctions[node.name as VariableName] = sym
}
for ( const statement of Object.values(this.statements) ) {
graph.addNode(statement.id, statement)
}
for ( const statement of Object.values(this.statements) ) {
for ( const symbol of statement.uses() ) {
const functionProvider = definedFunctions[symbol.name as VariableName]
if ( functionProvider ) {
graph.addDependency(statement.id, functionProvider.id)
continue
}
const provider = defined[symbol.name as VariableName]
if ( !provider ) {
throw new Error('No provider for undefined symbol: ' + symbol.name)
}
graph.addDependency(statement.id, provider.id)
}
}
return graph
}
/** Returns true if the given variable name is a function definition. */
isFunctionKey(name: VariableName): boolean {
return this.functions()
.some(stmt => {
const node = stmt.parse() as math.FunctionAssignmentNode
return node.name === name
})
}
/** Get all the statements defining functions. */
functions(): MathStatement[] {
return Object.values(this.statements)
.filter(x => x.isFunctionDeclaration())
}
/** Look up a function statement by name, if it exists. */
getFunctionByName(name: string): MathStatement|undefined {
for ( const fn of this.functions() ) {
const node = fn.parse() as math.FunctionAssignmentNode
if ( node.name === name ) {
return fn
}
}
}
/** Look up a function statement by name, if it exists. */
getFunctionByNameOrFail(name: string): MathStatement {
const fn = this.getFunctionByName(name)
if ( !fn ) {
throw new Error('Unable to find function with name: ' + name)
}
return fn
}
/** Evaluate the current state of the page and get the result. */
evaluate(): EvaluationResult {
const evaluations: Record<StatementID, any> = {}
const scope: Record<VariableName, any> = {}
const graph = this.dependencies()
const definers = this.definers()
for ( const stmt of this.functions() ) {
const node = stmt.parse() as math.FunctionAssignmentNode
scope[node.name as VariableName] = stmt.parse().evaluate()
}
for ( const node of graph.overallOrder() ) {
const stmt = this.statements[node as StatementID]
evaluations[stmt.id] = stmt.parse()
.compile()
.evaluate(scope)
}
const nonFunctionalScope: Record<VariableName, any> = {}
for ( const key in scope ) {
if ( !hasOwnProperty(scope, key) ) {
continue
}
if ( definers[key as VariableName] ) {
nonFunctionalScope[key as VariableName] = scope[key]
}
}
return {
variables: nonFunctionalScope,
statements: evaluations,
scope,
}
}
}