Implement MathPage evaluation logic

This commit is contained in:
Garrett Mills 2022-04-09 02:50:20 -05:00
parent 2aa2a5bf05
commit be66844f17
3 changed files with 69 additions and 9 deletions

View File

@ -2,58 +2,77 @@ import {MathStatement} from './parse'
import * as math from 'mathjs'
import {DepGraph} from 'dependency-graph'
import { v4 as uuidv4 } from 'uuid'
import {EvaluationResult, StatementID, VariableName} from '../types'
/**
* Wrapper for a page containing multiple interrelated mathematical statements.
*/
export class MathPage {
protected statements: Record<string, MathStatement> = {}
/** The statements on the page. */
protected statements: Record<StatementID, MathStatement> = {}
constructor(
/** Unique page ID. */
public readonly id: string,
) {}
/** 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): this {
return this.addStatement(new MathStatement(uuidv4(), statement))
return this.addStatement(new MathStatement(uuidv4() as StatementID, statement))
}
/** 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), [])
}
dependencies(): DepGraph<MathStatement> {
const graph = new DepGraph<MathStatement>()
const defined: Record<string, MathStatement> = {}
/** 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() ) {
defined[symbol.name] = statement
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()
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 provider = defined[symbol.name]
const provider = defined[symbol.name as VariableName]
if ( !provider ) {
throw new Error('No provider for undefined symbol: ' + symbol.name)
}
@ -64,4 +83,23 @@ export class MathPage {
return graph
}
/** 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()
for ( const node of graph.overallOrder() ) {
const stmt = this.statements[node as StatementID]
evaluations[stmt.id] = stmt.parse()
.compile()
.evaluate(scope)
}
return {
variables: scope,
statements: evaluations,
}
}
}

View File

@ -1,6 +1,6 @@
import * as math from 'mathjs'
import katex from 'katex'
import {HTMLString, LaTeXString} from '../types'
import {HTMLString, LaTeXString, StatementID} from '../types'
/** Base class for walks over MathNode trees. */
export abstract class MathNodeWalk<TReturn> {
@ -72,6 +72,7 @@ export abstract class MathNodeWalk<TReturn> {
}
/** A walk that accumulates all different SymbolNode instances in a tree. */
export class SymbolWalk extends MathNodeWalk<math.SymbolNode[]> {
walkAccessorNode(node: math.AccessorNode): math.SymbolNode[] {
return [
@ -168,6 +169,7 @@ export class SymbolWalk extends MathNodeWalk<math.SymbolNode[]> {
}
/** A walk that accumulates all SymbolNode instances used on the RHS of expressions. */
export class RValSymbolWalk extends SymbolWalk {
walkAssignmentNode(node: math.AssignmentNode): math.SymbolNode[] {
return this.walk(node.value)
@ -184,6 +186,7 @@ export class RValSymbolWalk extends SymbolWalk {
}
/** A walk that accumulates SymbolNode instances used on the LHS of assignments. */
export class LValSymbolWalk extends SymbolWalk {
walkAccessorNode(): math.SymbolNode[] {
return []
@ -252,26 +255,35 @@ export class LValSymbolWalk extends SymbolWalk {
}
}
/** A single mathematical statement. */
export class MathStatement {
constructor(
public readonly id: string,
/** Unique ID of this statement. */
public readonly id: StatementID,
/** The raw statement input by the user. */
public readonly raw: string,
) {}
/** Parse the raw statement to an AST. */
parse(): math.MathNode {
return math.parse(this.raw)
}
/** Convert the statement to its equivalent LaTeX code. */
toLaTeX(): LaTeXString {
return this.parse().toTex() as LaTeXString
}
/** Render the statement as HTML string. */
toHTMLString(): HTMLString {
return katex.renderToString(this.toLaTeX(), {
output: 'mathml',
}) as HTMLString
}
/** Render the statement to a DOM element. */
toDOM(): HTMLSpanElement {
const node = document.createElement('span')
katex.render(this.toLaTeX(), node, {
@ -280,14 +292,17 @@ export class MathStatement {
return node
}
/** Get all symbols referenced in this statement. */
symbols(): math.SymbolNode[] {
return (new SymbolWalk()).walk(this.parse())
}
/** Get all symbols defined on the LHS of this statement. */
defines(): math.SymbolNode[] {
return (new LValSymbolWalk()).walk(this.parse())
}
/** Get all symbols used on the RHS of this statement. */
uses(): math.SymbolNode[] {
return (new RValSymbolWalk()).walk(this.parse())
}

View File

@ -83,3 +83,10 @@ export function isInteger(num: number): num is Integer {
export type LaTeXString = TypeTag<'@app.LaTeXString'> & string
export type HTMLString = TypeTag<'@app.HTMLString'> & string
export type StatementID = TypeTag<'@app.StatementID'> & string
export type VariableName = TypeTag<'@app.VariableName'> & string
export interface EvaluationResult {
variables: Record<VariableName, any>
statements: Record<StatementID, any>
}