Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add debug calc for sr-formula #1408

Merged
merged 7 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions libs/sr-formula/src/calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AnyNode, CalcResult } from '@genshin-optimizer/pando'
import { Calculator as Base, calculation } from '@genshin-optimizer/pando'
import { assertUnreachable } from '@genshin-optimizer/util'
import type { Tag } from './data/util'
import { DebugCalculator } from './debug'

const { arithmetic } = calculation

Expand Down Expand Up @@ -68,4 +69,7 @@ export class Calculator extends Base<Output> {
assertUnreachable(op)
}
}
toDebug(): DebugCalculator {
return new DebugCalculator(this)
}
}
36 changes: 36 additions & 0 deletions libs/sr-formula/src/data/util/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,42 @@ export function tagVal(cat: keyof Tag): TagValRead {
return baseTagVal(cat)
}

export function tagStr(tag: Tag, ex?: any): string {
const { name, member, dst, et, src, q, qt, dt, move, ...remaining } = tag

if (Object.keys(remaining).length) console.error(remaining)

let result = '{ ',
includedRequired = false,
includedBar = false
function required(str: string | undefined | null) {
if (!str) return
result += str + ' '
includedRequired = true
}
function optional(str: string | undefined | null) {
if (!str) return
if (includedRequired && !includedBar) {
includedBar = true
result += '| '
}
result += str + ' '
}
required(name && `#${name}`)
required(member)
required(dst && `(${dst})`)
required(src)
required(et)
if (qt && q) required(`${qt}.${q}`)
else if (qt) required(`${qt}.`)
else if (q) required(`.${q}`)

optional(dt)
optional(move)
required(ex && `[${ex}]`)
return result + '}'
}

export const reader = new Read({}, undefined)
export const usedNames = new Set<string>()
export const usedQ = new Set('_')
167 changes: 162 additions & 5 deletions libs/sr-formula/src/debug.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import type {
AnyNode,
CalcResult,
ReRead,
TagMapSubsetCache,
} from '@genshin-optimizer/pando'
import { TagMapExactValues, traverse } from '@genshin-optimizer/pando'
import type { Read, Tag } from './data/util'
import type { Calculator } from './calculator'
import {
Calculator as BaseCalculator,
TagMapExactValues,
TagMapKeys,
traverse,
} from '@genshin-optimizer/pando'
import { type Calculator } from './calculator'
import { keys } from './data'
import { tagStr, type Read, type Tag, type TagMapNodeEntry } from './data/util'

const tagKeys = new TagMapKeys(keys)
export const debugKey = Symbol('tagSource')

export function dependencyString(read: Read, calc: Calculator) {
const str = listDependencies(read.tag, calc).map(({ tag, read, reread }) => {
Expand All @@ -30,8 +40,7 @@ export function listDependencies(
const result: { tag: Tag; read: Tag[]; reread: Tag[] }[] = [],
stack: Tag[] = []
/** Stack depth when first encountered the tag, or 0 if already visited */
const tagKeys = calc.keys
const openDepth = new TagMapExactValues<number>(tagKeys.tagLen, {})
const openDepth = new TagMapExactValues<number>(keys.tagLen, {})

function internal(cache: TagMapSubsetCache<AnyNode | ReRead>) {
const tag = cache.tag,
Expand Down Expand Up @@ -81,3 +90,151 @@ export function listDependencies(
internal(calc.nodes.cache(calc.keys).with(tag))
return result
}

export function printEntry({ tag, value }: TagMapNodeEntry): string {
function printNode(node: AnyNode): string {
const { op, tag, br, x } = node
let { ex } = node
if (op === 'const') return JSON.stringify(ex)
if (op === 'read') return `${node}`
if (op === 'subscript') ex = undefined
const args: string[] = []
if (ex) args.push(JSON.stringify(ex))
if (tag) args.push(tagStr(tag))
args.push(...br.map(printNode), ...x.map(printNode))
return `${op}(` + args.join(', ') + ')'
}

if (value.op === 'reread') return tagStr(tag) + ` <- ` + tagStr(value.tag)
return tagStr(tag) + ` <= ` + printNode(value)
}

export type DebugMeta = {
valText: string
text: string
deps: DebugMeta[]
}
export class DebugCalculator extends BaseCalculator<DebugMeta> {
constructor(calc: Calculator) {
super(calc.keys)
this.nodes = calc.nodes
}
override computeMeta(
n: AnyNode,
val: number | string,
x: (CalcResult<number | string, DebugMeta> | undefined)[],
br: CalcResult<number | string, DebugMeta>[],
tag: Tag | undefined
): DebugMeta {
const result: DebugMeta = {
valText: valStr(val),
text: '',
deps: [],
}

function toStr(
x: CalcResult<number | string, DebugMeta> | undefined
): string {
if (!x) return ''
result.deps.push(...x.meta.deps)
return x.meta.text
}
function valStr(val: number | string): string {
if (typeof val !== 'number') return `"${val}"`
if (Math.round(val) === val) return `${val}`
return val.toFixed(2)
}

const { op, ex, tag: nTag } = n
switch (op) {
case 'const':
result.text = result.valText
break
case 'read': {
const args = x as CalcResult<number | string, DebugMeta>[]
const text = `read ${tagStr(nTag!, ex)}`
return {
valText: result.valText,
text,
deps: [
{
valText: result.valText,
text: `match ${tagStr(tag!, ex)} for ${text}`,
deps: args.map(({ meta, entryTag }) => ({
...meta,
text: `${entryTag
?.map((tag) => (tag ? tagStr(tag) : '(unknown tag)'))
.join(' <- ')} <= ${meta.text}`,
})),
},
],
}
}
case 'match':
case 'thres':
case 'lookup': {
const chosen = x.find((x) => x)!
result.text = `${op}(${br.map(toStr).join(', ')} => ${toStr(chosen)})`
break
}
case 'subscript': {
const [index] = br
const chosen = valStr(ex[index.val as number]!)
result.text = `subscript(${toStr(index)} => ${chosen})`
break
}
case 'tag':
result.text = `tag(${tagStr(nTag!)}, ${toStr(x[0])})`
break
case 'dtag':
result.text = `dtag(${br
.map((br, i) => `${ex[i]} => ${toStr(br)}`)
.join(', ')}; ${toStr(x[0])})`
break
default: {
const specialArgs = ex ? [JSON.stringify(ex)] : []
const brArgs = br.map(toStr)
const xArgs = x.map(toStr)
const args = [specialArgs, brArgs, xArgs].map((x) => x.join(', '))

result.text = `${op}(` + args.filter((x) => x.length).join('; ') + ')'
}
}
return result
}

debugCompute(node: AnyNode): string[] {
const { meta } = this.compute(node)
const result: string[] = []
addDebugString(meta, 0, result, new Set())
return result
}

debugGet(tag: Tag): string[] {
const list = this.get(tag)
const result: string[] = []
const found = new Set<DebugMeta>()
for (const { meta } of list) {
addDebugString(meta, 0, result, found)
}
return result
}
}

function addDebugString(
meta: DebugMeta,
level: number,
result: string[],
found: Set<DebugMeta>
) {
const indent = Array(2 * level + 1).join(' ')
const line = `${meta.valText} ${meta.text}`

if (found.has(meta)) {
result.push(indent + line + ' (Dup)')
} else {
found.add(meta)
result.push(indent + line)
meta.deps.forEach((dep) => addDebugString(dep, level + 1, result, found))
}
}
1 change: 1 addition & 0 deletions libs/sr-formula/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { compileTagMapValues, constant } from '@genshin-optimizer/pando'
import { Calculator } from './calculator'
import { keys, values } from './data'
export * from './data/util'
export * from './debug'

export function srCalculatorWithValues(extras: TagMapEntries<number>) {
return srCalculatorWithEntries(
Expand Down