@@ -1136,6 +1136,33 @@ export class BoxedFunction
11361136 return this . engine . _fn ( 'List' , results ) ;
11371137 }
11381138
1139+ //
1140+ // 2b/ Broadcast user-defined function literals over indexed collections
1141+ // When a function defined via `ce.assign('f', x \mapsto ...)` is applied
1142+ // to a list (or other finite indexed collection) and the function's
1143+ // parameters are scalar, map the function over the collection.
1144+ // Note: tuples satisfy `isFiniteIndexedCollection` and are intentionally
1145+ // included — a fixed-size tuple of scalars behaves like a small vector.
1146+ //
1147+ if (
1148+ def . _isLambda &&
1149+ this . ops ! . some ( ( x ) => isFiniteIndexedCollection ( x ) ) &&
1150+ paramsAreScalar ( def )
1151+ ) {
1152+ const items = zip ( this . _ops ) ;
1153+ if ( items ) {
1154+ const results : Expression [ ] = [ ] ;
1155+ while ( true ) {
1156+ const { done, value } = items . next ( ) ;
1157+ if ( done ) break ;
1158+ results . push (
1159+ this . engine . _fn ( this . operator , value ) . evaluate ( options )
1160+ ) ;
1161+ }
1162+ return this . engine . _fn ( 'List' , results ) ;
1163+ }
1164+ }
1165+
11391166 //
11401167 // 3/ Handle evaluation of lazy collections
11411168 //
@@ -1226,6 +1253,31 @@ export class BoxedFunction
12261253 ) ;
12271254 }
12281255
1256+ //
1257+ // 2b/ Broadcast user-defined function literals over indexed collections.
1258+ // Mirrors the sync path in `_computeValue`.
1259+ //
1260+ if (
1261+ def ?. _isLambda &&
1262+ this . ops ! . some ( ( x ) => isFiniteIndexedCollection ( x ) ) &&
1263+ paramsAreScalar ( def )
1264+ ) {
1265+ const items = zip ( this . _ops ) ;
1266+ if ( items ) {
1267+ const results : Promise < Expression > [ ] = [ ] ;
1268+ while ( true ) {
1269+ const { done, value } = items . next ( ) ;
1270+ if ( done ) break ;
1271+ results . push (
1272+ this . engine . _fn ( this . operator , value ) . evaluateAsync ( options )
1273+ ) ;
1274+ }
1275+ return Promise . all ( results ) . then ( ( resolved ) =>
1276+ this . engine . _fn ( 'List' , resolved )
1277+ ) ;
1278+ }
1279+ }
1280+
12291281 //
12301282 // 3/ Evaluate the applicable operands
12311283 //
@@ -1570,10 +1622,98 @@ function applyFunctionLiteral(
15701622 if ( ! value || value . type . isUnknown )
15711623 return expr . engine . function ( expr . operator , ops ) ;
15721624
1625+ // Broadcast if any operand is a finite indexed collection and the
1626+ // function's parameter types are scalar. Zip operands and apply
1627+ // pointwise, returning a List of results. Tuples count as indexed
1628+ // collections, so a tuple of scalars also triggers broadcasting.
1629+ if (
1630+ ops . some ( ( x ) => isFiniteIndexedCollection ( x ) ) &&
1631+ paramsAreScalar ( value . type . type )
1632+ ) {
1633+ const items = zip ( ops ) ;
1634+ if ( items ) {
1635+ const results : Expression [ ] = [ ] ;
1636+ while ( true ) {
1637+ const { done, value : zipped } = items . next ( ) ;
1638+ if ( done ) break ;
1639+ results . push ( apply ( value , zipped ) . evaluate ( options ) ) ;
1640+ }
1641+ return expr . engine . _fn ( 'List' , results ) ;
1642+ }
1643+ }
1644+
15731645 // The value is a function literal. Apply the arguments to it
15741646 return apply ( value , ops ) ;
15751647}
15761648
1649+ /** Returns true when every formal parameter of a signature is a scalar
1650+ * type (not a collection/list/tuple/function).
1651+ *
1652+ * Accepts either a `Type` (typically from a function-typed value) or a
1653+ * `BoxedOperatorDefinition` (whose `signature.type` is inspected).
1654+ *
1655+ * Conservative: unknown/any and non-signature types are treated as scalar,
1656+ * which makes this a permissive default for inferred lambda signatures.
1657+ * @internal
1658+ */
1659+ function paramsAreScalar ( source : BoxedOperatorDefinition | Type ) : boolean {
1660+ const sigType = isOperatorDefinition ( source )
1661+ ? source . signature ?. type
1662+ : source ;
1663+ if ( ! sigType || typeof sigType === 'string' ) return true ;
1664+ if ( sigType . kind !== 'signature' ) return true ;
1665+ const args = [
1666+ ...( sigType . args ?? [ ] ) ,
1667+ ...( sigType . optArgs ?? [ ] ) ,
1668+ ...( sigType . variadicArg ? [ sigType . variadicArg ] : [ ] ) ,
1669+ ] ;
1670+ return args . every ( ( arg ) => isScalarType ( arg . type ) ) ;
1671+ }
1672+
1673+ function isOperatorDefinition (
1674+ source : BoxedOperatorDefinition | Type
1675+ ) : source is BoxedOperatorDefinition {
1676+ return (
1677+ typeof source === 'object' && source !== null && 'signature' in source
1678+ ) ;
1679+ }
1680+
1681+ /** A type is "scalar" for broadcasting purposes if it is NOT a known
1682+ * collection-like type. Conservative: unknown/any → scalar.
1683+ */
1684+ function isScalarType ( t : Type ) : boolean {
1685+ if ( typeof t === 'string' ) {
1686+ // String types like 'collection', 'list', 'tuple', 'set' are non-scalar.
1687+ if (
1688+ t === 'collection' ||
1689+ t === 'indexed_collection' ||
1690+ t === 'list' ||
1691+ t === 'tuple' ||
1692+ t === 'set' ||
1693+ t === 'dictionary' ||
1694+ t === 'record' ||
1695+ t === 'function'
1696+ )
1697+ return false ;
1698+ return true ;
1699+ }
1700+ if (
1701+ t . kind === 'collection' ||
1702+ t . kind === 'indexed_collection' ||
1703+ t . kind === 'list' ||
1704+ t . kind === 'tuple' ||
1705+ t . kind === 'set' ||
1706+ t . kind === 'dictionary' ||
1707+ t . kind === 'record' ||
1708+ t . kind === 'signature'
1709+ )
1710+ return false ;
1711+ if ( t . kind === 'union' || t . kind === 'intersection' )
1712+ return t . types . every ( ( x ) => isScalarType ( x ) ) ;
1713+ if ( t . kind === 'negation' ) return isScalarType ( t . type ) ;
1714+ return true ;
1715+ }
1716+
15771717/** Eagerly evaluate xs by iterating over its elements.
15781718 *
15791719 * If eager is true, evaluate DEFAULT_MATERIALIZATION elements.
@@ -1598,6 +1738,14 @@ function materialize(
15981738 const isIndexed = expr . isIndexedCollection ;
15991739 const isFinite = expr . isFiniteCollection ;
16001740
1741+ // Leave oversized indexed collections in their lazy form. Consumers
1742+ // can detect the size via `.count` without risking OOM.
1743+ if ( isIndexed && isFinite ) {
1744+ const count = expr . count ;
1745+ if ( count !== undefined && count > expr . engine . maxCollectionSize )
1746+ return expr ;
1747+ }
1748+
16011749 const xs : Expression [ ] = [ ] ;
16021750
16031751 if ( ! expr . isEmptyCollection ) {
0 commit comments