Skip to content

Commit 782e651

Browse files
authored
Merge pull request #60 from thiremani/feat-array-comparison-mask
feat(compiler): element-wise array comparison masks + per-slot condition propagation
2 parents 7f40582 + 89e3471 commit 782e651

16 files changed

Lines changed: 2198 additions & 624 deletions

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,11 @@ b = Square(2.2) # float specialization
143143
c = Square(arr) # squares each array element
144144
a, b, c
145145

146-
# Range and filter (non-accumulated)
146+
# Range, mask, and filter (non-accumulated)
147147
d = Square(1:3) # range: final iteration result
148148
e = Square(arr[1:3]) # array-range: final iteration result
149-
f = Square(arr > 3) # filtered array
150-
g = Square(arr[1:3] > 3) # filtered array-range
149+
f = Square(arr > 3) # element-wise mask: each element kept where > 3, else 0
150+
g = Square(arr[1:3] > 3) # array-range filter: final iteration result
151151
d, e, f, g
152152

153153
# Accumulation forms
@@ -162,11 +162,11 @@ Output:
162162

163163
```text
164164
25 4.84 [1 4 9 25]
165-
4 9 [25] 0
165+
4 9 [0 0 0 25] 0
166166
[1 4] [4 9] [0 4] [0 9 25]
167167
```
168168

169-
No generics syntax, no type parameters. Write the template once — call it with a scalar, an array, or a filtered view. `arr > 3` filters the array to elements greater than 3 and passes that subset through.
169+
No generics syntax, no type parameters. Write the template once — call it with a scalar, an array, or a masked array. `arr > 3` masks the array element-wise — each element kept where it's greater than 3, else 0 (same length) — and passes that through.
170170

171171
Generated code is equivalent to handwritten specialized code. There is no runtime overhead.
172172

@@ -195,7 +195,7 @@ x = [1 2 3 4 5]
195195
y = [1.1 2.2 3.3]
196196
```
197197

198-
Arrays are safe by construction — out-of-bounds access is not possible. Comparisons like `arr > 2` produce filtered views that work anywhere an array does.
198+
Arrays are safe by construction — out-of-bounds access is not possible. Comparisons like `arr > 2` produce element-wise masks (each element kept where it holds, else 0) that work anywhere an array does.
199199

200200
---
201201

compiler/array.go

Lines changed: 58 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,16 @@ func (c *Compiler) forEachArrayPair(
607607
})
608608
}
609609

610+
// arraySym wraps an array vector and element type as a Symbol, bitcast to the
611+
// opaque i8* array handle used for array values.
612+
func (c *Compiler) arraySym(array llvm.Value, elemType Type) *Symbol {
613+
i8p := llvm.PointerType(c.Context.Int8Type(), 0)
614+
return &Symbol{
615+
Type: Array{Headers: nil, ColTypes: []Type{elemType}, Length: 0},
616+
Val: c.builder.CreateBitCast(array, i8p, "arr_i8p"),
617+
}
618+
}
619+
610620
func (c *Compiler) compileArrayArrayInfix(op string, left *Symbol, right *Symbol, resElem Type) *Symbol {
611621
leftArrType := left.Type.(Array)
612622
rightArrType := right.Type.(Array)
@@ -639,10 +649,7 @@ func (c *Compiler) compileArrayArrayInfix(op string, left *Symbol, right *Symbol
639649
})
640650

641651
// Return result array
642-
i8p := llvm.PointerType(c.Context.Int8Type(), 0)
643-
resSym := &Symbol{Type: Array{Headers: nil, ColTypes: []Type{resElem}, Length: 0}}
644-
resSym.Val = c.builder.CreateBitCast(resVec, i8p, "arr_i8p")
645-
return resSym
652+
return c.arraySym(resVec, resElem)
646653
}
647654

648655
func (c *Compiler) compileArrayConcat(left *Symbol, right *Symbol, leftElem Type, rightElem Type, resElem Type) *Symbol {
@@ -666,10 +673,7 @@ func (c *Compiler) compileArrayConcat(left *Symbol, right *Symbol, leftElem Type
666673
c.CopyArrayInto(resVec, right, rightElem, resElem, leftLen, true)
667674

668675
// Return concatenated array
669-
i8p := llvm.PointerType(c.Context.Int8Type(), 0)
670-
resSym := &Symbol{Type: Array{Headers: nil, ColTypes: []Type{resElem}, Length: 0}}
671-
resSym.Val = c.builder.CreateBitCast(resVec, i8p, "arr_i8p")
672-
return resSym
676+
return c.arraySym(resVec, resElem)
673677
}
674678

675679
func (c *Compiler) compileArrayScalarInfix(op string, arr *Symbol, scalar *Symbol, resElem Type, arrayOnLeft bool) *Symbol {
@@ -695,83 +699,74 @@ func (c *Compiler) compileArrayScalarInfix(op string, arr *Symbol, scalar *Symbo
695699
c.ArraySetOwnForType(resElem, resVec, iter, resultVal)
696700
})
697701

698-
i8p := llvm.PointerType(c.Context.Int8Type(), 0)
699-
resSym := &Symbol{Type: Array{Headers: nil, ColTypes: []Type{resElem}, Length: 0}}
700-
resSym.Val = c.builder.CreateBitCast(resVec, i8p, "arr_i8p")
701-
return resSym
702+
return c.arraySym(resVec, resElem)
702703
}
703704

704-
// compileArrayFilter dispatches value-position comparison filtering when at least
705-
// one operand is an array. It keeps LHS values where the comparison holds.
706-
// For scalar-op-array, the scalar LHS is repeated for each true element.
707-
func (c *Compiler) compileArrayFilter(op string, left *Symbol, right *Symbol, expected Type) *Symbol {
705+
// compileArrayMask dispatches value-position comparison masking when at least one
706+
// operand is an array. Each position yields the LHS element where the comparison
707+
// holds, otherwise 0 — preserving length and position (a 0 marks a failed compare,
708+
// never a dropped element). This is Pluto's scalar "comparison yields LHS-or-0"
709+
// applied per element. Array-array zips to min length; array-scalar uses the
710+
// array's length and broadcasts the scalar.
711+
func (c *Compiler) compileArrayMask(op string, left *Symbol, right *Symbol, expected Type) *Symbol {
708712
l := c.derefIfPointer(left, "")
709713
r := c.derefIfPointer(right, "")
710-
711-
resArr := expected.(Array)
712-
acc := c.NewArrayAccumulator(resArr)
714+
resElem := expected.(Array).ColTypes[0]
713715

714716
if l.Type.Kind() == ArrayKind && r.Type.Kind() == ArrayKind {
715-
return c.compileArrayArrayFilter(op, l, r, acc)
717+
return c.compileArrayArrayMask(op, l, r, resElem)
716718
}
717719
if l.Type.Kind() == ArrayKind {
718-
return c.compileArrayScalarFilter(op, l, r, acc, true)
720+
return c.compileArrayScalarMask(op, l, r, resElem, true)
719721
}
720722
if r.Type.Kind() == ArrayKind {
721-
return c.compileArrayScalarFilter(op, r, l, acc, false)
723+
return c.compileArrayScalarMask(op, r, l, resElem, false)
722724
}
723-
panic(fmt.Sprintf("compileArrayFilter expects at least one array operand, got %s and %s", l.Type, r.Type))
725+
panic(fmt.Sprintf("compileArrayMask expects at least one array operand, got %s and %s", l.Type, r.Type))
724726
}
725727

726-
// filterPush conditionally appends sym to acc based on cond.
727-
// Emits a branch: if cond is true, push sym; otherwise skip.
728-
func (c *Compiler) filterPush(acc *ArrayAccumulator, sym *Symbol, cond llvm.Value) {
729-
copyBlock, nextBlock := c.createIfCont(cond, "filter_copy", "filter_next")
728+
// maskStore writes lhs into resVec[iter] when cond holds; on failure it leaves the
729+
// slot at its preallocated zero (0 for numbers, "" for strings), realizing
730+
// `cond ? lhs : 0` per cell. The set copies the (borrowed) element, so the source
731+
// array keeps ownership and the result owns an independent copy.
732+
func (c *Compiler) maskStore(resElem Type, resVec llvm.Value, iter llvm.Value, lhs *Symbol, cond llvm.Value) {
733+
setBlock, contBlock := c.createIfCont(cond, "mask_set", "mask_cont")
730734

731-
c.builder.SetInsertPointAtEnd(copyBlock)
732-
c.PushVal(acc, sym)
733-
c.builder.CreateBr(nextBlock)
735+
c.builder.SetInsertPointAtEnd(setBlock)
736+
c.ArraySetForType(resElem, resVec, iter, lhs.Val)
737+
c.builder.CreateBr(contBlock)
734738

735-
c.builder.SetInsertPointAtEnd(nextBlock)
739+
c.builder.SetInsertPointAtEnd(contBlock)
736740
}
737741

738-
func (c *Compiler) compileArrayArrayFilter(op string, left *Symbol, right *Symbol, acc *ArrayAccumulator) *Symbol {
739-
c.forEachArrayPair(left, right, c.arrayPairMinLen(left, right), func(_ llvm.Value, leftSym *Symbol, rightSym *Symbol) {
740-
cmpResult := defaultOps[opKey{
741-
Operator: op,
742-
LeftType: opType(leftSym.Type.Key()),
743-
RightType: opType(rightSym.Type.Key()),
744-
}](c, leftSym, rightSym, true)
745-
746-
c.filterPush(acc, leftSym, cmpResult.Val)
742+
func (c *Compiler) compileArrayArrayMask(op string, left *Symbol, right *Symbol, resElem Type) *Symbol {
743+
loopLen := c.arrayPairMinLen(left, right)
744+
resVec := c.CreateArrayForType(resElem, loopLen)
745+
c.forEachArrayPair(left, right, loopLen, func(iter llvm.Value, leftSym *Symbol, rightSym *Symbol) {
746+
lhs, cond := c.compareScalars(op, leftSym, rightSym)
747+
c.maskStore(resElem, resVec, iter, lhs, cond)
747748
})
748749

749-
return c.ArrayAccResult(acc)
750+
return c.arraySym(resVec, resElem)
750751
}
751752

752-
func (c *Compiler) compileArrayScalarFilter(op string, arr *Symbol, scalar *Symbol, acc *ArrayAccumulator, arrayOnLeft bool) *Symbol {
753+
func (c *Compiler) compileArrayScalarMask(op string, arr *Symbol, scalar *Symbol, resElem Type, arrayOnLeft bool) *Symbol {
753754
arrElem := arr.Type.(Array).ColTypes[0]
754-
c.forEachArrayPair(arr, scalar, c.ArrayLen(arr, arrElem), func(_ llvm.Value, elemSym *Symbol, scalarSym *Symbol) {
755-
leftSym := elemSym
756-
rightSym := scalarSym
757-
pushSym := elemSym
755+
loopLen := c.ArrayLen(arr, arrElem)
756+
resVec := c.CreateArrayForType(resElem, loopLen)
757+
c.forEachArrayPair(arr, scalar, loopLen, func(iter llvm.Value, elemSym *Symbol, scalarSym *Symbol) {
758+
// Preserve operand order for non-commutative comparisons. The masked value
759+
// is always the comparison's LHS (the array element, or the scalar when it
760+
// is on the left), which compareScalars returns alongside the result.
761+
leftSym, rightSym := elemSym, scalarSym
758762
if !arrayOnLeft {
759-
// Preserve original operand order for non-commutative comparisons,
760-
// and keep scalar LHS values on true comparisons.
761-
leftSym = scalarSym
762-
rightSym = elemSym
763-
pushSym = scalarSym
763+
leftSym, rightSym = scalarSym, elemSym
764764
}
765-
cmpResult := defaultOps[opKey{
766-
Operator: op,
767-
LeftType: opType(leftSym.Type.Key()),
768-
RightType: opType(rightSym.Type.Key()),
769-
}](c, leftSym, rightSym, true)
770-
771-
c.filterPush(acc, pushSym, cmpResult.Val)
765+
lhs, cond := c.compareScalars(op, leftSym, rightSym)
766+
c.maskStore(resElem, resVec, iter, lhs, cond)
772767
})
773768

774-
return c.ArrayAccResult(acc)
769+
return c.arraySym(resVec, resElem)
775770
}
776771

777772
func (c *Compiler) compileArrayUnaryPrefix(op string, arr *Symbol, result Array) *Symbol {
@@ -799,10 +794,7 @@ func (c *Compiler) compileArrayUnaryPrefix(op string, arr *Symbol, result Array)
799794
c.ArraySetOwnForType(resElem, resVec, idx, resultVal)
800795
})
801796

802-
i8p := llvm.PointerType(c.Context.Int8Type(), 0)
803-
resSym := &Symbol{Type: result}
804-
resSym.Val = c.builder.CreateBitCast(resVec, i8p, "arr_i8p")
805-
return resSym
797+
return c.arraySym(resVec, resElem)
806798
}
807799

808800
// Array string conversion function
@@ -916,8 +908,9 @@ func (c *Compiler) compileArrayRangeBasic(expr *ast.ArrayRangeExpression) []*Sym
916908
}}
917909
}
918910
// Scalar element access does not retain the source array pointer.
919-
// Release temporary array sources on all scalar return paths.
920-
defer c.freeTemporary(expr.Array, []*Symbol{arraySym})
911+
// Release temporary array sources on all scalar return paths; a consumed
912+
// frame mask ((arr > 1)[i]) is recorded so the sweep skips it.
913+
defer c.freeConsumedTemporary(expr.Array, []*Symbol{arraySym})
921914

922915
// Scalar index - element access
923916
arrElemType := arrType.ColTypes[0]

0 commit comments

Comments
 (0)