import * as L from './lang.js' import { detailsToCompleteString, ICE } from './result.js' import * as P from './parser.js' import * as Runtime from './runtime.js' // N.B., Because we output to HTML, we must take care to escape HTML entities. // But we can't escape all the entities---we want to leave the angle brackets // that should ACTUALLY be interpreted as html tags! In the future, we should // rely on hacky two-phase output to render html, but for now, we have to // do a "deep" sanitization of entities, escaping in all cases except when we // emit actual html tags. const sanitize = (s: string, htmlOutput: boolean): string => htmlOutput ? s.replace(/&/g, '&').replace(//g, '>') : s const namedCharTable = new Map(Array.from(P.namedCharValues.entries()).map(([name, value]) => [value, name])) function charToName (s: string): string { return namedCharTable.get(s) ?? s } function litToString (l: L.Lit, htmlOutput: boolean): string { switch (l.tag) { case 'bool': return l.value ? '#t' : '#f' case 'num': return l.value.toString() case 'char': return `#\\${sanitize(charToName(l.value), htmlOutput)}` case 'str': return `"${sanitize(l.value, htmlOutput)}"` } } function isSimpleExp (e: L.Exp): boolean { switch (e.tag) { case 'value': return true case 'var': return true case 'lit': return true case 'call': return e.args.every(isSimpleExp) case 'lam': return false case 'if': return false case 'nil': return true case 'pair': return true case 'let': return false case 'cond': return false case 'and': return true case 'or': return true case 'match': return false case 'begin': return false } } function nestingDepth (e: L.Exp): number { switch (e.tag) { case 'value': return 0 case 'var': return 0 case 'lit': return 0 case 'call': return 1 + Math.max(...e.args.map(nestingDepth)) case 'lam': return 1 + nestingDepth(e.body) case 'if': return 1 + Math.max(nestingDepth(e.e1), nestingDepth(e.e2), nestingDepth(e.e3)) case 'nil': return 0 case 'pair': return 1 + Math.max(nestingDepth(e.e1), nestingDepth(e.e2)) case 'let': return 0 // TODO case 'cond': return 0 // TODO case 'and': return 0 // TODO case 'or': return 0 // TODO case 'match': return 0 // TODO case 'begin': return 0 // TODO } } function parens (bracketKind: L.BracketKind, ss: string[], sep: string = ' '): string { switch (bracketKind) { case '(': return `(${ss.join(sep)})` case '{': return `{${ss.join(sep)}}` case '[': return `[${ss.join(sep)}]` } } function indent (col: number, s: string): string { return `${' '.repeat(col)}${s}` } function patToString (p: L.Pat, htmlOutput: boolean): string { switch (p.tag) { case 'var': return sanitize(p.id, htmlOutput) case 'wild': return '_' case 'null': return 'null' case 'lit': return litToString(p.lit, htmlOutput) case 'ctor': return parens('(', [p.head, ...p.args.map(p => patToString(p, htmlOutput))]) } } export function valueToString (col: number, v: L.Value, htmlOutput: boolean = false): string { if (typeof v === 'boolean') { return v ? '#t' : '#f' } else if (typeof v === 'number') { return v.toString() } else if (typeof v === 'string') { return `"${sanitize(v, htmlOutput)}"` } else if (L.valueIsChar(v)) { const ch = (v as L.CharType).value let printed = ch switch (ch) { // TODO: probably add in special cases for... all the other cases! case ' ': printed = 'space'; break default: break } return `#\\${sanitize(printed, htmlOutput)}` } else if (L.valueIsLambda(v) || L.valueIsPrim(v)) { return '[object Function]' } else if (L.valueIsPair(v)) { return L.valueIsList(v) ? `(list ${L.valueListToArray_(v).map(v => valueToString(col, v, htmlOutput)).join(' ')})` : `(cons ${valueToString(col, (v as L.PairType).fst, htmlOutput)} ${valueToString(col, (v as L.PairType).snd, htmlOutput)})` } else if (L.valueIsStruct(v)) { const s = v as L.StructType return s.fields.length === 0 ? `(struct ${s.kind.toString()} ())` : `(struct ${s.kind.toString()} ${s.fields.map(v => valueToString(col, v, htmlOutput)).join(' ')})` } else if (Array.isArray(v)) { return v.length === 0 ? '(vector)' : `(vector ${v.map(v => valueToString(col, v, htmlOutput)).join(' ')})` } else if (v === null) { return 'null' } else if (v === undefined) { return 'void' } else if (typeof v === 'object') { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (Object.hasOwn(v, 'renderAs') && (v as any).renderAs === 'audio') { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access (v as any).storeTag = Runtime.store.add((v as any).data) return htmlOutput ? `${sanitize(JSON.stringify(v), htmlOutput)}` : '[object AudioPipeline]' // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access } else if (Object.hasOwn(v, 'renderAs') && (v as any).renderAs === 'drawing') { return htmlOutput ? `${sanitize(JSON.stringify(v), htmlOutput)}` : '[object Drawing]' // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access } else if (Object.hasOwn(v, 'renderAs') && (v as any).renderAs === 'composition') { const tag = Runtime.store.add(v) return htmlOutput ? `` : '[object Composition]' // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access } else if ('tagName' in v) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access const tag = Runtime.store.add(v) return htmlOutput // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions ? `` : `[object Element:${(v as Element).tagName}]` // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access } else if (Object.hasOwn(v, 'renderAs') && (v as any).renderAs === 'audio-pipeline') { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const tag = Runtime.store.add(v) return htmlOutput // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions ? `` : '[object AudioPipeline]' // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access } else if (Object.hasOwn(v, 'renderAs') && (v as any).renderAs === 'reactive-file') { const tag = Runtime.store.add(v) return htmlOutput // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions ? `` : '[object ReactiveFile]' } else { return '[object Object]' } } // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new ICE('valueToString', `unknown value encountered: ${v}`) } export function expToString (col: number, e: L.Exp, htmlOutput: boolean = false): string { switch (e.tag) { case 'value': return valueToString(col, e.value, htmlOutput) case 'var': return sanitize(e.value, htmlOutput) case 'lit': return litToString(e.value, htmlOutput) case 'call': { const allExps = [e.head, ...e.args] if (allExps.every(isSimpleExp) && allExps.every(e => nestingDepth(e) <= 4) && e.args.length <= 5) { return parens(e.bracket, [e.head].concat(e.args).map(arg => expToString(col, arg, htmlOutput))) } else { return parens(e.bracket, [ `${expToString(col, e.head, htmlOutput)}`, ...e.args.map(arg => `${indent(col + 2, expToString(col + 2, arg, htmlOutput))}`) ], '\n') } } case 'lam': { const preamble = `(lambda ${parens(e.bracket, e.args.map(n => n.value))}` if (isSimpleExp(e.body)) { return [preamble, `${expToString(col, e.body, htmlOutput)})`].join(' ') } else { return [preamble, `${indent(col + 2, expToString(col + 2, e.body, htmlOutput))})`].join('\n') } } case 'if': { return parens(e.bracket, [ `if ${expToString(col, e.e1, htmlOutput)}`, `${indent(col + 2, expToString(col + 2, e.e2, htmlOutput))}`, `${indent(col + 2, expToString(col + 2, e.e3, htmlOutput))}` ], '\n') } case 'nil': return 'null' case 'pair': return e.isList ? parens(e.bracket, ['list'].concat(L.unsafeListToArray(e).map(arg => expToString(col, arg, htmlOutput)))) : parens(e.bracket, ['cons', expToString(col, e.e1, htmlOutput), expToString(col, e.e2, htmlOutput)]) case 'let': { const preamble = `${e.kind} ` // N.B., this bracket is difficult to factor out using paren... const firstBinding = `${indent(col + 2, `([${e.bindings[0][0].value} ${expToString(col + 2 + e.bindings[0][0].value.length + 1, e.bindings[0][1], htmlOutput)}]`)}` const bindings = e.bindings.length === 1 ? firstBinding + ')' : [firstBinding, ...e.bindings.slice(1).map(b => `${indent(col + 2 + 1, `[${b[0].value} ${expToString(col + 2 + 1 + b[0].value.length + 1, b[1], htmlOutput)}]`)}`)].join('\n') + ')' const body = `${indent(col + 2, `${expToString(col + 2, e.body, htmlOutput)}`)}` return parens(e.bracket, [preamble, bindings, body], '\n') } case 'cond': { const preamble = 'cond ' const bindings = e.branches.map(b => indent(col + 2, `[${expToString(col + 2, b[0], htmlOutput)} ${expToString(col + 2, b[1], htmlOutput)}]`)) return parens(e.bracket, [preamble, ...bindings], '\n') } case 'and': return parens(e.bracket, ['and', ...e.args.map(arg => expToString(col + 2, arg, htmlOutput))]) case 'or': return parens(e.bracket, ['or', ...e.args.map(arg => expToString(col + 2, arg, htmlOutput))]) case 'match': { const bindings = e.branches.map(b => indent(col + 2, `[${patToString(b[0], htmlOutput)} ${expToString(col + 2, b[1], htmlOutput)}]`)) return parens(e.bracket, [`match ${expToString(col, e.scrutinee, htmlOutput)}`, ...bindings], '\n') } case 'begin': { return parens(e.bracket, ['begin', ...e.exps.map(e => expToString(0, e, htmlOutput))]) } } } export function effectToString (col: number, effect: L.SEffect, outputBindings: boolean = false, htmlOutput: boolean = false): string { switch (effect.tag) { case 'error': return effect.errors.map(err => detailsToCompleteString(err)).join('\n') case 'binding': return outputBindings ? `[[${sanitize(effect.name, htmlOutput)} bound]]` : '' case 'testresult': { if (effect.passed) { return `[[ Test "${sanitize(effect.desc, htmlOutput)}" passed! ]]` } else { const msg: string = effect.reason ? effect.reason : ` Expected: ${expToString(col, effect.expected!, htmlOutput)}\n Actual: ${expToString(col, effect.actual!, htmlOutput)}` return `[[ Test "${sanitize(effect.desc, htmlOutput)}" failed!\n${msg}\n]]` } } case 'value': return valueToString(col, effect.output, htmlOutput) case 'imported': return outputBindings ? `[[${sanitize(effect.source, htmlOutput)} imported]]` : '' } } export function stmtToString (col: number, stmt: L.Stmt, outputBindings: boolean = false, htmlOutput: boolean = false): string { switch (stmt.tag) { case 'define': { const preamble = `(define ${sanitize(stmt.name.value, htmlOutput)}` return isSimpleExp(stmt.value) ? `${preamble} ${expToString(col, stmt.value, htmlOutput)})` : `${preamble}\n${indent(col + 2, expToString(col + 2, stmt.value, htmlOutput))})` } case 'struct': return `(struct ${sanitize(stmt.id.value, htmlOutput)} (${stmt.fields.map(f => f.value).join(' ')}))` case 'testcase': return `(test-case ${expToString(col, stmt.desc, htmlOutput)} ${expToString(col, stmt.comp, htmlOutput)}\n${indent(col + 2, expToString(col + 2, stmt.expected, htmlOutput))}\n${indent(col + 2, expToString(col + 2, stmt.actual, htmlOutput))})` case 'exp': return expToString(col, stmt.value, htmlOutput) case 'import': return `(import ${sanitize(stmt.source, htmlOutput)})` case 'error': return effectToString(col, stmt, outputBindings, htmlOutput) case 'binding': return effectToString(col, stmt, outputBindings, htmlOutput) case 'testresult': return effectToString(col, stmt, outputBindings, htmlOutput) case 'value': return effectToString(col, stmt, outputBindings, htmlOutput) case 'imported': return effectToString(col, stmt, outputBindings, htmlOutput) } } export function progToString ( col: number, prog: L.Program, outputBindings: boolean = false, htmlOutput: boolean = false, lineSep: string = '\n'): string { return prog .map(s => stmtToString(col, s, outputBindings, htmlOutput)) .filter(s => s.length > 0) .map(s => htmlOutput ? `${s}` : s) .join(lineSep) }