diff --git a/src/compute-engine/latex-syntax/dictionary/definitions-arithmetic.ts b/src/compute-engine/latex-syntax/dictionary/definitions-arithmetic.ts index 9e1cc477..2117c3ff 100755 --- a/src/compute-engine/latex-syntax/dictionary/definitions-arithmetic.ts +++ b/src/compute-engine/latex-syntax/dictionary/definitions-arithmetic.ts @@ -1358,48 +1358,86 @@ function parseBigOp(name: string, reduceOp: string, minPrec: number) { }; } -function serializeBigOp(command: string) { - return (serializer, expr) => { - if (!operand(expr, 1)) return command; +const INDEXING_SET_HEADS = new Set([ + 'Tuple', + 'Triple', + 'Pair', + 'Single', + 'Limits', +]); + +function sanitizeLimitOperand( + expr: Expression | null | undefined +): Expression | null { + if (expr === null || expr === undefined) return null; + if (symbol(expr) === 'Nothing') return null; + return expr; +} - let arg = operand(expr, 2); - const h = operator(arg); - if (h !== 'Tuple' && h !== 'Triple' && h !== 'Pair' && h !== 'Single') - arg = null; +function collectIndexingSets(expr: Expression): Expression[] { + const result: Expression[] = []; + const args = operands(expr); + if (args.length <= 1) return result; + for (const candidate of args.slice(1)) { + const head = operator(candidate); + if (head && INDEXING_SET_HEADS.has(head)) { + result.push(candidate); + continue; + } + break; + } + return result; +} - let index = operand(arg, 1); - if (index !== null && operator(index) === 'Hold') index = operand(index, 1); +function serializeIndexingSet( + serializer: Serializer, + indexingSet: Expression +): { sub?: string; sup?: string } { + let indexExpr = operand(indexingSet, 1); + if (indexExpr !== null && operator(indexExpr) === 'Hold') + indexExpr = operand(indexExpr, 1); - const fn = operand(expr, 1); + const lowerExpr = sanitizeLimitOperand(operand(indexingSet, 2)); + const upperExpr = sanitizeLimitOperand(operand(indexingSet, 3)); - if (arg !== null && arg !== undefined) { - if (operand(expr, 2) !== null) - return joinLatex([command, serializer.serialize(fn)]); - return joinLatex([ - command, - '_{', - serializer.serialize(operand(expr, 2)), - '}', - serializer.serialize(fn), - ]); - } + const result: { sub?: string; sup?: string } = {}; + const indexName = indexExpr ? symbol(indexExpr) : null; + const hasIndex = indexName !== null && indexName !== 'Nothing'; + const indexLatex = + hasIndex && indexExpr ? serializer.serialize(indexExpr) : undefined; - const lower = operand(arg, 2); + if (hasIndex && lowerExpr !== null && indexLatex) + result.sub = `${indexLatex}=${serializer.serialize(lowerExpr)}`; + else if (hasIndex && indexLatex) result.sub = indexLatex; + else if (lowerExpr !== null) result.sub = serializer.serialize(lowerExpr); - let sub: string[] = []; - if (index && symbol(index) !== 'Nothing' && lower) - sub = [serializer.serialize(index), '=', serializer.serialize(lower)]; - else if (index && symbol(index) !== 'Nothing') - sub = [serializer.serialize(index)]; - else if (lower !== null) sub = [serializer.serialize(lower)]; + if (upperExpr !== null) result.sup = serializer.serialize(upperExpr); - if (sub.length > 0) sub = ['_{', ...sub, '}']; + return result; +} - let sup: string[] = []; - if (operand(arg, 3) !== null) - sup = ['^{', serializer.serialize(operand(arg, 3)), '}']; +function serializeBigOp(command: string) { + return (serializer: Serializer, expr: Expression): string => { + const body = operand(expr, 1); + if (!body) return command; + + const indexingSets = collectIndexingSets(expr); + let decoratedCommand = command; + if (indexingSets.length > 0) { + const subs: string[] = []; + const sups: string[] = []; + for (const set of indexingSets) { + const parts = serializeIndexingSet(serializer, set); + if (parts.sub) subs.push(parts.sub); + if (parts.sup) sups.push(parts.sup); + } + if (subs.length > 0) + decoratedCommand = supsub('_', decoratedCommand, subs.join(', ')); + if (sups.length > 0) + decoratedCommand = supsub('^', decoratedCommand, sups.join(', ')); + } - return joinLatex([command, ...sup, ...sub, serializer.serialize(fn)]); + return joinLatex([decoratedCommand, serializer.serialize(body)]); }; } diff --git a/src/compute-engine/library/utils.ts b/src/compute-engine/library/utils.ts index 86f57235..a837d146 100755 --- a/src/compute-engine/library/utils.ts +++ b/src/compute-engine/library/utils.ts @@ -237,6 +237,24 @@ export function canonicalIndexingSet( let upper: BoxedExpression | null = null; let lower: BoxedExpression | null = null; + // If this is already a canonical Limits expression, return it (after + // canonicalizing its operands) so re-canonicalization paths (like `subs`) + // preserve the bounds. + if (expr.operator === 'Limits') { + const canonicalIndex = expr.op1.canonical; + const canonicalLower = expr.op2?.canonical ?? ce.Nothing; + const canonicalUpper = expr.op3?.canonical ?? ce.Nothing; + if (!canonicalIndex.symbol) + return ce.function('Limits', [ + ce.typeError('symbol', undefined, canonicalIndex), + ]); + return ce.function('Limits', [ + canonicalIndex, + canonicalLower, + canonicalUpper, + ]); + } + if ( expr.operator === 'Tuple' || expr.operator === 'Triple' || @@ -274,9 +292,9 @@ export function canonicalBigop( // Note: we need to canonicalize the indexes before canonicalizing the body // since we need the indexes to be declared before we can bind them - const indexes = indexingSets - .map((x) => canonicalIndexingSet(x)) - .filter((x) => x ?? ce.error('missing')) as BoxedExpression[]; + const indexes = indexingSets.map( + (x) => canonicalIndexingSet(x) ?? ce.error('missing') + ); body = body?.canonical ?? ce.error('missing'); diff --git a/test/compute-engine/latex-syntax/serialize.test.ts b/test/compute-engine/latex-syntax/serialize.test.ts index 1553ebc5..810a8fc4 100755 --- a/test/compute-engine/latex-syntax/serialize.test.ts +++ b/test/compute-engine/latex-syntax/serialize.test.ts @@ -136,6 +136,15 @@ describe('LATEX SERIALIZING', () => { `\\int\\!\\sin(x)\\, \\mathrm{d}x` ); }); + + test('Big operators', () => { + expect(ce.parse('\\sum_{k=0}^{100}k').toLatex()).toMatchInlineSnapshot( + `\\sum_{k=0}^{100}k` + ); + expect(ce.parse('\\prod_{i=1}^{n}i').toLatex()).toMatchInlineSnapshot( + `\\prod_{i=1}^{n}i` + ); + }); }); describe('CUSTOM LATEX SERIALIZING', () => {