Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 are write-only inside the template — they flow out; use a local for intermediate values. 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
62 changes: 13 additions & 49 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 @@ -49,7 +48,7 @@ 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
// The alias pattern names 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.
Expand Down Expand Up @@ -79,28 +78,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 +106,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 +117,9 @@ func (abi FuncABI) SourceFunctionParamIndex(paramIndex int) int {
return abi.sourceParamBaseIndex() + paramIndex
}

func (abi FuncABI) AliasParamBaseIndex() int {
return abi.sourceParamBaseIndex() + len(abi.Params)
}

func (abi FuncABI) AliasFunctionParamIndex(paramIndex int) int {
slot := abi.Params[paramIndex].AliasSlot
if slot < 0 {
return -1
}
return abi.AliasParamBaseIndex() + slot
}

func (abi FuncABI) DirectReturnSeedParamIndex() int {
if abi.Return.Mode != ABIReturnDirect {
return -1
}
return abi.AliasParamBaseIndex() + abi.NumAliasSlots()
return abi.sourceParamBaseIndex() + len(abi.Params)
}
94 changes: 71 additions & 23 deletions compiler/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,14 @@ func (cfg *CFG) validateFuncTemplate(fn *ast.FuncStatement) {
PushScope(&cfg.Scopes, FuncScope)
defer PopScope(&cfg.Scopes)

// Outputs are published up front so that a formatting marker naming one
// resolves as a read and is rejected, instead of passing as literal text.
for _, param := range fn.Parameters {
cfg.publishTarget(param)
}
for _, output := range fn.Outputs {
cfg.publishTarget(output)
}

parameterNames := make(map[string]struct{}, len(fn.Parameters))
for _, parameter := range fn.Parameters {
Expand All @@ -207,7 +212,8 @@ func (cfg *CFG) validateFuncTemplate(fn *ast.FuncStatement) {
outputNames[output.Value] = struct{}{}
}

_, readInputs, assignedOutputs := cfg.validateTemplateBody(fn.Body.Statements, parameterNames, outputNames)
body := cfg.validateTemplateBody(fn.Body.Statements, parameterNames, outputNames)
readInputs, assignedOutputs := body.readInputs, body.assignedOutputs

for _, input := range fn.Parameters {
if _, wasRead := readInputs[input.Value]; wasRead {
Expand All @@ -225,35 +231,43 @@ func (cfg *CFG) validateFuncTemplate(fn *ast.FuncStatement) {
}
}

// validateTemplateBody runs structural validation over one template body and
// returns each statement's reads plus the parameter and output names the body
// read and assigned. A script is a zero-input, zero-output template: it passes
// nil name sets and consumes only the reads.
func (cfg *CFG) validateTemplateBody(statements []ast.Statement, parameterNames, outputNames map[string]struct{}) ([][]VarEvent, map[string]struct{}, map[string]struct{}) {
statementReads := make([][]VarEvent, 0, len(statements))
readInputs := make(map[string]struct{}, len(parameterNames))
assignedOutputs := make(map[string]struct{}, len(outputNames))
// templateBody is the structural summary of one template body.
type templateBody struct {
statementReads [][]VarEvent
readInputs map[string]struct{}
assignedOutputs map[string]struct{}
}

// validateTemplateBody runs structural validation over one template body. A
// script is a zero-input, zero-output template: it passes nil name sets and
// consumes only the reads.
func (cfg *CFG) validateTemplateBody(statements []ast.Statement, parameterNames, outputNames map[string]struct{}) templateBody {
body := templateBody{
statementReads: make([][]VarEvent, 0, len(statements)),
readInputs: make(map[string]struct{}, len(parameterNames)),
assignedOutputs: make(map[string]struct{}, len(outputNames)),
}
for _, stmt := range statements {
reads := cfg.collectStatementReads(stmt)
targets := cfg.validateStatementStructure(stmt, reads, parameterNames)
targets := cfg.validateStatementStructure(stmt, reads, parameterNames, outputNames)
if let, ok := stmt.(*ast.LetStatement); ok {
cfg.publishTargets(let.Name)
}

statementReads = append(statementReads, reads)
body.statementReads = append(body.statementReads, reads)
for _, event := range reads {
if _, isParameter := parameterNames[event.Name]; isParameter {
readInputs[event.Name] = struct{}{}
body.readInputs[event.Name] = struct{}{}
}
}
for _, target := range targets {
if _, isOutput := outputNames[target.Value]; isOutput {
assignedOutputs[target.Value] = struct{}{}
body.assignedOutputs[target.Value] = struct{}{}
}
}
}

return statementReads, readInputs, assignedOutputs
return body
}

// AnalyzeScript treats the script as a zero-input, zero-output template before
Expand All @@ -278,8 +292,7 @@ func (cfg *CFG) validateScriptTemplate(statements []ast.Statement) [][]VarEvent
PushScope(&cfg.Scopes, BlockScope)
defer PopScope(&cfg.Scopes)

statementReads, _, _ := cfg.validateTemplateBody(statements, nil, nil)
return statementReads
return cfg.validateTemplateBody(statements, nil, nil).statementReads
}

// AnalyzeSpecialization runs only typed dataflow. Structural diagnostics were
Expand All @@ -294,7 +307,7 @@ func (cfg *CFG) AnalyzeSpecialization(template *ast.FuncStatement, info *FuncInf
cfg.publishTarget(param)
}

cfg.typedForwardPass(template.Body.Statements, info.StatementEffects)
cfg.typedForwardPass(template, info)

live := make(map[string]struct{}, len(template.Outputs))
for _, output := range template.Outputs {
Expand All @@ -303,11 +316,39 @@ func (cfg *CFG) AnalyzeSpecialization(template *ast.FuncStatement, info *FuncInf
cfg.backwardPass(live)
}

func (cfg *CFG) typedForwardPass(statements []ast.Statement, effects map[*ast.LetStatement]StatementEffect) {
// inputOutputAliases lists outputs that a caller could share with each input.
// Specializations are reused across calls, so liveness must conservatively
// retain writes observable through any compatible input reference. These are
// scalar body types, so this also conservatively includes iterator inputs.
func inputOutputAliases(template *ast.FuncStatement, info *FuncInfo) map[string][]*ast.Identifier {
aliases := make(map[string][]*ast.Identifier, len(template.Parameters))
for i, paramType := range info.Sig.Params {
for j, outputType := range info.Sig.OutTypes {
if !bindingSlotCompatible(paramType, outputType) {
continue
}
name := template.Parameters[i].Value
aliases[name] = append(aliases[name], template.Outputs[j])
}
}

return aliases
}

func (cfg *CFG) typedForwardPass(template *ast.FuncStatement, info *FuncInfo) {
aliases := inputOutputAliases(template, info)
lastWrites := make(map[string]VarEvent)
for _, stmt := range statements {
for _, stmt := range template.Body.Statements {
reads := cfg.collectStatementReads(stmt)
cfg.processTypedStatement(stmt, reads, effects, lastWrites)
for _, read := range reads {
for _, output := range aliases[read.Name] {
if !cfg.isDefined(output.Value) {
continue
}
reads = append(reads, VarEvent{Name: output.Value, Kind: Read, Token: read.Token})
}
}
cfg.processTypedStatement(stmt, reads, info.StatementEffects, lastWrites)
}
}

Expand All @@ -334,9 +375,9 @@ func (cfg *CFG) processTypedStatement(stmt ast.Statement, reads []VarEvent, effe
// validateStatementStructure reports template-stable read and write errors and
// returns named targets for caller-specific bookkeeping. The caller publishes
// them only after all statement reads have been checked.
func (cfg *CFG) validateStatementStructure(stmt ast.Statement, reads []VarEvent, parameters map[string]struct{}) []*ast.Identifier {
func (cfg *CFG) validateStatementStructure(stmt ast.Statement, reads []VarEvent, parameters, outputs map[string]struct{}) []*ast.Identifier {
for _, event := range reads {
cfg.validateStructuralRead(event)
cfg.validateStructuralRead(event, outputs)
}

let, ok := stmt.(*ast.LetStatement)
Expand Down Expand Up @@ -445,7 +486,14 @@ func (cfg *CFG) backwardPass(live map[string]struct{}) {
}
}

func (cfg *CFG) validateStructuralRead(event VarEvent) {
// validateStructuralRead enforces that a declared output is write-only inside
// its template. A body may observe output writes through an explicitly passed
// input that shares the output's binding, but never through the output name.
func (cfg *CFG) validateStructuralRead(event VarEvent, outputs map[string]struct{}) {
if _, isOutput := outputs[event.Name]; isOutput {
cfg.addError(event.Token, fmt.Sprintf("output %q is read inside its function; outputs are write-only, use a local", event.Name))
return
}
if !cfg.isDefined(event.Name) {
cfg.addError(event.Token, fmt.Sprintf("variable %q has not been defined", event.Name))
}
Expand Down
5 changes: 3 additions & 2 deletions compiler/cfg_replay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,9 @@ result = Diamond(x)

func TestCFGResultsAreIndependentPerType(t *testing.T) {
code := mustParseCode(t, `result = MaskOrKeep(x)
result = x
result = x > 0
local = x
result = local
result = local > 0
`)

ctx := llvm.NewContext()
Expand Down
Loading
Loading