Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
56d2f48
feat(compiler): make function outputs write-only and inputs iteration…
thiremani Sep 11, 2026
7387308
docs(effects): record PR #104 and issue #103 in the seed disposition
thiremani Sep 11, 2026
1eac2fe
perf(compiler): snapshot only inputs an output could alias
thiremani Sep 11, 2026
19dcfe3
refactor(compiler): snapshot every aliasable input, drop lateInputReads
thiremani Sep 11, 2026
92bddd8
fix(llvm): preserve loop metadata when adding unroll hints
thiremani Sep 12, 2026
1f1b208
feat(compiler): preserve live input references across output writes
thiremani Sep 12, 2026
31d9977
docs(memory): state the in-body read order and explicit-save rule
thiremani Sep 12, 2026
38c4eea
refactor(compiler): decide input/output aliasing at compile time
thiremani Sep 12, 2026
8c862a2
refactor(compiler): mangle lowering variants with marker suffixes
thiremani Sep 12, 2026
ceb8a89
fix(compiler): keep alias identity through ranged staging and conditi…
thiremani Sep 12, 2026
03d9970
docs(abi): state the 2.1 prototype change precisely across the plans
thiremani Sep 12, 2026
3819f65
test(compiler): make the array alias regression bite and retire selec…
thiremani Sep 12, 2026
30dad11
refactor(cfg): declare names instead of storing phantom write events
thiremani Sep 15, 2026
6294117
docs(cfg): name the shared-input liveness rule as an over-approximation
thiremani Sep 19, 2026
4fc9847
feat(cfg): analyze function liveness exactly per alias context
thiremani Sep 19, 2026
652678a
fix(cfg): carry widened output storage into nested call contexts
thiremani Sep 19, 2026
a1e6c35
docs(abi): state variant suffix normalization and fix the storage exa…
thiremani Sep 20, 2026
5df1e1d
refactor(compiler): derive shared output storage from the alias pattern
thiremani Sep 20, 2026
49ce683
refactor(compiler): drop outcome-neutral guards from the alias machinery
thiremani Sep 20, 2026
f3ed70a
fix(cfg): keep parameters in scope for the typed pass's marker reads
thiremani Sep 20, 2026
6a2012d
feat(compiler): allow reading an output once it is definitely assigned
thiremani Sep 20, 2026
1c19da0
fix(compiler): promote only string outputs; structs share at exact type
thiremani Sep 20, 2026
8e13419
refactor(cfg): name the two read steps of the typed forward pass
thiremani Sep 20, 2026
2dd5b7c
fix(cfg): name the input-initialization repair in the output-read error
thiremani Sep 21, 2026
6056c0a
refactor(compiler): fold the sharing rule into abi.go and file a stra…
thiremani Sep 21, 2026
fbd6ab7
refactor(cfg): inline the shared-input read loop in the typed pass
thiremani Sep 21, 2026
f1774df
docs(effects): point the archived seed analysis at the follow-up issue
thiremani Sep 21, 2026
08c7777
refactor(effects): return definite targets as a set
thiremani Sep 22, 2026
3973f1a
test(cfg): restore the per-type CFG test's direct input reads
thiremani Sep 22, 2026
e1585de
feat(cfg): make output-write liveness independent of callers
thiremani Sep 22, 2026
79ff74b
docs(cfg): state the liveness policy precisely and keep the widening …
thiremani Sep 22, 2026
83548b9
docs(memory): say the double write is reported as dead, not that it i…
thiremani Sep 22, 2026
1366a0c
refactor(compiler): inline the read-outputs lookup
thiremani Sep 22, 2026
ccff923
docs(compiler): drop the stale CFG reference from the alias pattern c…
thiremani Sep 22, 2026
2221295
docs(compiler): reflow the alias pattern comment
thiremani Sep 22, 2026
f1a34ac
refactor(compiler): scope synthetic conditional destinations to the s…
thiremani Sep 22, 2026
a760820
refactor(compiler): key parameter aliases by name and symbol
thiremani Sep 22, 2026
1dc6b14
test(alias): make the two-inputs-one-output fixture bite
thiremani Sep 22, 2026
6460660
test(alias): pin the shared range parameter's scalar iterator
thiremani Sep 22, 2026
53540d3
fix(compiler): index conditional-call parameters by expanded position
thiremani Sep 22, 2026
628a9ed
fix(compiler): keep shared inputs read-only and loop-safe across staging
thiremani Sep 23, 2026
8cb2452
fix(solver): type untyped call arguments by their own value unless sh…
thiremani Sep 23, 2026
a99bca7
fix(compiler): stage indirect-call outputs after evaluating arguments
thiremani Sep 23, 2026
af3085b
test: restore fixtures that read outputs after definite assignment
thiremani Sep 23, 2026
0f37335
docs: correct stale statements about arguments, ABI 2.1 and %n
thiremani Sep 23, 2026
a54a48a
revert(llvm): move loop metadata preservation to its own PR
thiremani Sep 23, 2026
e7dbf79
docs(compiler): say aliasPattern's destinations arrive resolved
thiremani Sep 23, 2026
cba83c1
refactor(compiler): show an alias variant's sharing inside its signature
thiremani Sep 23, 2026
2a8ebde
fix(solver): carry storage ownership through comparison arguments
thiremani Sep 23, 2026
3fa2a74
fix(compiler): reject heap arguments to static parameters at lowering
thiremani Sep 23, 2026
61b249e
refactor(solver): name the call-argument storage rules
thiremani Sep 23, 2026
0983a46
docs(solver): describe argumentStorage by what it returns
thiremani Sep 23, 2026
6c358e0
test(solver): restore master's recursion and refinement tests
thiremani Sep 23, 2026
dca7db7
test: separate fixture cases with blank lines
thiremani Sep 23, 2026
bda75c7
test(call_arg_types): cover comparison arguments whose chain fails
thiremani Sep 24, 2026
aa467cd
test(struct): put the text after an interpolated struct on its own line
thiremani Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Compile and run:

Templates are defined once with a clear input/output contract. The first line declares the output and input — the indented body describes the transformation.

Think of a template as a **black box**: data flows in through inputs, gets transformed, and flows out through outputs. Outputs work **by reference** — calling a template directly modifies the output variable in the caller's scope.
Think of a template as a **black box**: data flows in through inputs, gets transformed, and flows out through outputs. Outputs work **by reference**. A caller may connect an input and an output to the same variable; inside the call, later input reads observe writes through that output. The caller's variable receives the result after every right-hand side of the assignment has been evaluated.

`math.pt`
```python
Expand All @@ -134,7 +134,18 @@ y = Square(x)
y = x * x
```

Inputs are read-only — they flow in. Outputs are writable — they flow out. Every function is a transformation.
Inputs are read-only — they flow in. Outputs flow out: the template may read one only after assigning it unconditionally, as in `sq = x * x` followed by `cube = sq * x`; before that, use a local. Read-only means the template cannot assign through the input name; it does not freeze a value shared with an output. A caller may reuse a variable as both argument and destination, `a = Square(a)`.

```python
out, seen = Fold(current, item)
out = current + item
seen = current

value = 10
value, seen = Fold(value, 5) # value = 15, seen = 15
```

Moving `seen = current` before `out = current + item` instead makes `seen` equal 10. The same order applies to each iteration of a ranged call. To keep an old value across a write, save it first with an explicit assignment; that is where any copy happens.

### Generics by use

Expand Down
116 changes: 59 additions & 57 deletions compiler/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ const (
)

type ABIParam struct {
Source Type
Lowered Type
Mode ABIParamMode
AliasSlot int
Source Type
Lowered Type
Mode ABIParamMode
}

type ABIReturn struct {
Expand All @@ -29,12 +28,12 @@ type ABIReturn struct {

// FuncABI captures the lowered function boundary for one mangled variant.
// Direct scalar returns carry a hidden destination seed so a skipped write
// preserves the caller's value. Range-bearing variants may additionally need
// hidden alias state for loop-carried accumulation.
// preserves the caller's value. Whether an input shares a caller binding with
// an output is a compile-time property of each call site, lowered as a private
// variant of the function; it never appears in the native signature.
type FuncABI struct {
Params []ABIParam
Return ABIReturn
HasRangeParams bool
Params []ABIParam
Return ABIReturn
}

func isDirectScalarABIType(t Type) bool {
Expand All @@ -48,18 +47,6 @@ func isDirectScalarABIType(t Type) bool {
}
}

// aliasableOutput reports whether an output can back a parameter's alias slot.
// The hidden selector picks an output by position and the callee then reads that
// storage as the parameter's own type, so the two must lower identically. There
// is no numeric conversion anywhere on this path, and a pointer selected across
// mismatched types would be loaded as the wrong type.
func aliasableOutput(paramType, outputType Type) bool {
if ptr, ok := outputType.(Ptr); ok {
outputType = ptr.Elem
}
return TypeEqual(paramType, outputType)
}

func directScalarABIReturnType(outTypes []Type) (Type, bool) {
if len(outTypes) != 1 {
return nil, false
Expand All @@ -79,28 +66,15 @@ func classifyFuncABI(paramTypes []Type, outTypes []Type) FuncABI {
},
}

for _, paramType := range paramTypes {
if isRangeDriverType(paramType) {
abi.HasRangeParams = true
break
}
}

aliasSlot := 0
for i, paramType := range paramTypes {
paramABI := ABIParam{
Source: paramType,
Lowered: Ptr{Elem: paramType},
Mode: ABIParamIndirect,
AliasSlot: -1,
Source: paramType,
Lowered: Ptr{Elem: paramType},
Mode: ABIParamIndirect,
}
if isDirectScalarABIType(paramType) {
paramABI.Mode = ABIParamDirect
paramABI.Lowered = paramType
if abi.HasRangeParams {
paramABI.AliasSlot = aliasSlot
aliasSlot++
}
}
abi.Params[i] = paramABI
}
Expand All @@ -120,16 +94,6 @@ func (abi FuncABI) UsesIndirectReturn() bool {
return abi.Return.Mode == ABIReturnIndirect
}

func (abi FuncABI) NumAliasSlots() int {
count := 0
for _, param := range abi.Params {
if param.AliasSlot >= 0 {
count++
}
}
return count
}

func (abi FuncABI) sourceParamBaseIndex() int {
if abi.UsesIndirectReturn() {
return 1
Expand All @@ -141,21 +105,59 @@ func (abi FuncABI) SourceFunctionParamIndex(paramIndex int) int {
return abi.sourceParamBaseIndex() + paramIndex
}

func (abi FuncABI) AliasParamBaseIndex() int {
func (abi FuncABI) DirectReturnSeedParamIndex() int {
if abi.Return.Mode != ABIReturnDirect {
return -1
}
return abi.sourceParamBaseIndex() + len(abi.Params)
}

func (abi FuncABI) AliasFunctionParamIndex(paramIndex int) int {
slot := abi.Params[paramIndex].AliasSlot
if slot < 0 {
return -1
// sharableOutput reports whether an input of paramType can share an output
// declared as outType: the input's storage must be the declared type or a
// compatible wider representation that a store converts (an owned string for
// a static output, a concrete-rank array for an untyped empty one, a schema
// for a header-only table). A struct shares only at its exact type, since
// nothing converts its fields. The shared output then uses the input's
// storage, so a write lands where the next read looks.
func sharableOutput(paramType, outType Type) bool {
if _, isStruct := outType.(Struct); isStruct {
return TypeEqual(paramType, outType)
}
return abi.AliasParamBaseIndex() + slot
return bindingSlotCompatible(paramType, outType) && TypeEqual(mergeBindingSlotType(paramType, outType), paramType)
}

func (abi FuncABI) DirectReturnSeedParamIndex() int {
if abi.Return.Mode != ABIReturnDirect {
return -1
// aliasPattern decides, per callee parameter, the one-based caller destination
// whose binding the argument shares, or 0; nil when no parameter shares one.
// argNames holds one entry per parameter, empty for an argument that is not a
// plain identifier. dests contains output destination names in order, with
// synthetic staging names already resolved to the bindings they represent.
// outTypes are the declared output types. enclosing maps a caller-body input to
// the caller output it already shares, so a nested call forwards that sharing.
// A parameter shares at most one destination, the first that matches.
func aliasPattern(argNames, dests []string, paramTypes, outTypes []Type, enclosing map[string]string) []int {
var pattern []int
for i, name := range argNames {
if name == "" {
continue
}

for j, dest := range dests {
if j >= len(outTypes) {
break
}
if !sharableOutput(paramTypes[i], outTypes[j]) {
continue
}
if dest != name && enclosing[name] != dest {
continue
}
if pattern == nil {
pattern = make([]int, len(argNames))
}
pattern[i] = j + 1
break
}
}
return abi.AliasParamBaseIndex() + abi.NumAliasSlots()

return pattern
}
5 changes: 5 additions & 0 deletions compiler/bounds.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,11 @@ func (c *Compiler) arraySymbolForAffineGuard(arrayExpr ast.Expression) (*Symbol,
if !ok {
return nil, nil, false
}
// A staged slot can change length inside the loop, so a bound checked
// once before it does not hold for every iteration.
if _, staged := c.currentStmtCtx().stagedSlots[raw.Val]; staged {
return nil, nil, false
}
arraySym := c.derefIfPointer(raw, ident.Value+"_affine_arr")
arrType, ok := arraySym.Type.(Array)
if !ok || arrType.ElemType == nil {
Expand Down
Loading
Loading