Skip to content

Commit 24e04ad

Browse files
committed
fix: #273
1 parent fb96703 commit 24e04ad

5 files changed

Lines changed: 616 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,33 @@
22

33
### New Features
44

5+
- **Type-Aware Subscript Handling**: Subscripts on symbols declared as collection
6+
types (list, tuple, matrix, etc.) now automatically convert to `At()` indexing
7+
operations:
8+
9+
```javascript
10+
ce.declare('v', 'list<number>');
11+
ce.parse('v_n'); // → At(v, n)
12+
ce.parse('v_{n+1}'); // → At(v, n+1)
13+
ce.parse('v_{i,j}'); // → At(v, Tuple(i, j))
14+
```
15+
16+
This works for both simple subscripts (`v_n`) and complex subscripts (`v_{n+1}`).
17+
The type of the `At()` expression is correctly inferred from the collection's
18+
element type, allowing subscripted collection elements to be used in arithmetic.
19+
20+
- **Complex Subscripts in Arithmetic** (Issue #273): Subscript expressions like
21+
`a_{n+1}` can now be used in arithmetic operations without type errors:
22+
23+
```javascript
24+
ce.parse('a_{n+1} + 1'); // → Add(Subscript(a, n+1), 1)
25+
ce.parse('2 * a_{n+1}'); // → Multiply(2, Subscript(a, n+1))
26+
ce.parse('a_{n+1}^2'); // → Power(Subscript(a, n+1), 2)
27+
```
28+
29+
Previously, complex subscripts would fail with "incompatible-type" errors
30+
when used in arithmetic contexts.
31+
532
- **Special Functions**: Added type signatures for special mathematical functions,
633
enabling them to be used in expressions without type errors:
734
- `Zeta` - Riemann zeta function ζ(s)

src/compute-engine/latex-syntax/parse-symbol.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,14 +283,22 @@ export function parseSymbol(parser: Parser): MathJsonSymbol | null {
283283
if (/^[a-zA-Z]$/.test(parser.peek) || /^\p{XIDS}$/u.test(parser.peek)) {
284284
let id = parser.nextToken();
285285

286+
// Check if the symbol is declared as a collection type.
287+
// If so, don't absorb subscripts into the symbol name - let them be
288+
// handled as Subscript expressions which will convert to At() calls.
289+
const symbolType = parser.getSymbolType(id);
290+
const isCollection = symbolType.matches('indexed_collection');
291+
286292
// Check if followed by subscript(s) - if so, include them in the symbol name
287293
// This ensures 'i_A' is parsed as a single symbol 'i_A', not as a subscript
288294
// applied to 'i' (which would turn 'i' into ImaginaryUnit first)
289295
// However, complex subscripts should remain as Subscript expressions:
290296
// - {n,m} (comma indicates multi-index)
291297
// - {n+1} (operators indicate an expression)
292298
// - {(n+1)} (parentheses indicate an expression)
293-
while (!parser.atEnd) {
299+
// Also, if the symbol is a collection type, all subscripts should be
300+
// Subscript expressions (which convert to At() calls).
301+
while (!parser.atEnd && !isCollection) {
294302
const currentPeek = parser.peek;
295303
if (currentPeek !== '_') break;
296304

src/compute-engine/library/collections.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,10 @@ export const COLLECTIONS_LIBRARY: SymbolDefinitions = {
810810
],
811811
complexity: 8200,
812812
signature: '(value: list|tuple|string, index: number | string) -> unknown',
813-
type: ([xs]) => xs.operatorDefinition?.collection?.elttype?.(xs) ?? 'any',
813+
type: ([xs]) =>
814+
xs.operatorDefinition?.collection?.elttype?.(xs) ??
815+
collectionElementType(xs.type.type) ??
816+
'any',
814817

815818
evaluate: (ops, { engine: ce }) => {
816819
// @todo: the implementation does not match the description. Need to think this through...

src/compute-engine/library/core.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,33 @@ export const CORE_LIBRARY: SymbolDefinitions[] = [
731731
if (op1.string && asSmallInteger(op2) !== null) return 'integer';
732732
if (op1.isIndexedCollection)
733733
return collectionElementType(op1.type.type) ?? 'any';
734-
if (op1.symbol) return 'symbol';
734+
735+
// Check if the symbol is declared as a collection type
736+
if (op1.symbol) {
737+
const eltType = collectionElementType(op1.type.type);
738+
if (eltType) return eltType;
739+
}
740+
741+
// For symbol bases with complex subscripts (like a_{n+1}), return 'unknown'
742+
// to allow type inference in arithmetic contexts. Simple subscripts
743+
// (like a_n) are converted to compound symbols during canonicalization
744+
// and won't reach this type function.
745+
if (op1.symbol) {
746+
// Check if this would become a compound symbol (simple subscript)
747+
const sub =
748+
op2.string ?? op2.symbol ?? asSmallInteger(op2)?.toString();
749+
if (sub) return 'symbol';
750+
// Check for InvisibleOperator of symbols/numbers (also becomes compound symbol)
751+
if (op2.operator === 'InvisibleOperator' && op2.ops) {
752+
const parts = op2.ops.map(
753+
(x) => x.symbol ?? asSmallInteger(x)?.toString()
754+
);
755+
if (parts.every((p) => p !== undefined && p !== null))
756+
return 'symbol';
757+
}
758+
// Complex subscript - return 'unknown' to allow numeric inference
759+
return 'unknown';
760+
}
735761
return 'expression';
736762
},
737763

@@ -754,9 +780,21 @@ export const CORE_LIBRARY: SymbolDefinitions[] = [
754780
]);
755781
}
756782

757-
// Is it a collection?
783+
// Is it a collection expression (like a list literal)?
758784
if (op1.isIndexedCollection) return ce._fn('At', [op1, op2.canonical]);
759785

786+
// Is it a symbol declared as a collection type?
787+
// If so, convert to At() for indexing
788+
if (op1.symbol && collectionElementType(op1.type.type)) {
789+
// For multi-index subscripts (Sequence/Tuple), pass each index as separate arg
790+
if (
791+
(op2.operator === 'Sequence' || op2.operator === 'Tuple') &&
792+
op2.ops
793+
)
794+
return ce._fn('At', [op1, ...op2.ops.map((x) => x.canonical)]);
795+
return ce._fn('At', [op1, op2.canonical]);
796+
}
797+
760798
// Is it a compound symbol `x_\operatorname{max}`, `\mu_0`
761799
if (op1.symbol) {
762800
const sub =

0 commit comments

Comments
 (0)