Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bdf7ea9
✨ feat(compiler): add conditional expressions in value position
thiremani Feb 13, 2026
ee5e281
🐛 fix(compiler): resolve cond-expr state isolation and range traversa…
thiremani Feb 13, 2026
c3baedb
style: run gofmt on compiler files
thiremani Feb 13, 2026
5420a71
refactor(compiler): replace cascade with extract-and-AND for cond-expr
thiremani Feb 13, 2026
dd9f39a
feat(compiler): support multi-result cond-expr and allow array compar…
thiremani Feb 14, 2026
a8eef33
fix(compiler): reject array-valued statement conditions at type-check…
thiremani Feb 14, 2026
7248887
✨ feat(compiler): add array filtering, CondMode enum, and cond-expr h…
thiremani Feb 15, 2026
a9678cb
refactor(compiler): unify tree-walking, extract helpers, add code rev…
thiremani Feb 15, 2026
0acfb7b
✨ feat(compiler): add string comparisons via strcmp, canonical Str ty…
thiremani Feb 16, 2026
d1447a9
refactor(compiler): extract RejectKind helper, remove dead InValueExp…
thiremani Feb 16, 2026
7d78748
refactor(compiler): flatten TypeLetStatement loop with guard clause
thiremani Feb 16, 2026
484a37e
refactor(compiler): inline CondMode classification, extract typeInfix…
thiremani Feb 16, 2026
3d44ee1
refactor(compiler): per-slot CompareModes, extract typeInfixSlot
thiremani Feb 16, 2026
60ffca2
fix(compiler): mixed array/scalar cond-expr panic, InValueExpr leak
thiremani Feb 17, 2026
eb82c70
refactor(compiler): avoid double deref in handleComparisons
thiremani Feb 17, 2026
9fa7f6f
refactor(compiler): use freeSymbolValue in handleComparisons
thiremani Feb 17, 2026
6d50eb3
fix(compiler): free CondArray results on false path, add AND coverage…
thiremani Feb 17, 2026
28a5bc3
fix(compiler): independent conditions for multi-target cond-expr
thiremani Feb 17, 2026
d8ea2be
test(cond): clean up value_cond_expr tests
thiremani Feb 17, 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
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ CI: GitHub Actions builds with Go 1.25, installs LLVM 21 + valgrind, and runs `p
- Test/validation command details are optional in commit messages; put full verification details in the PR description when possible.
- PRs: include a clear description, linked issues, unit/E2E tests for changes, and sample before/after output where relevant.

## Code Review Checklist

When reviewing PRs or preparing code for review, check:

1. **Modularity & readability**: Is each function focused on a single responsibility? Can a new reader follow the logic without excessive cross-referencing?
2. **Placement**: Do changes belong in the functions, arguments, and structs they touch, or should logic be moved to a more natural home?
3. **Duplication**: Is there code that duplicates existing patterns in the codebase? Extract shared logic into a helper or utility (e.g., `ast.ExprChildren` for tree-walking) rather than repeating type-switches or loop bodies.
4. **Nesting & control flow**: Can nested `if`/`for` blocks be flattened using early `return`, `continue`, or `break`? Prefer guard clauses over deep indentation.
5. **Naming**: Are new identifiers clear, consistent with existing conventions, and free of ambiguity? Avoid mixing synonyms (e.g., `tmp` vs `temp`) for the same concept.
6. **Edge cases**: Are zero-length slices, nil maps, and boundary values handled? Does the code distinguish "absent" from "empty"?
7. **Resource cleanup**: For compiler code specifically — are heap temporaries freed on all paths (true and false branches)? Are borrowed vs owned semantics respected?

## Debugging & Configuration Tips
- Quick smoke check: `./pluto tests/` to see compile/link output.
- Clear cache for current version: `./pluto --clean`
Expand Down
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ CI: GitHub Actions builds with Go 1.25, installs LLVM 21 + valgrind, and runs `p
- Test/validation command details are optional in commit messages; put full verification details in the PR description when possible.
- PRs: include a clear description, linked issues, unit/E2E tests for changes, and sample before/after output where relevant

## Code Review Checklist

When reviewing PRs or preparing code for review, check:

1. **Modularity & readability**: Is each function focused on a single responsibility? Can a new reader follow the logic without excessive cross-referencing?
2. **Placement**: Do changes belong in the functions, arguments, and structs they touch, or should logic be moved to a more natural home?
3. **Duplication**: Is there code that duplicates existing patterns in the codebase? Extract shared logic into a helper or utility (e.g., `ast.ExprChildren` for tree-walking) rather than repeating type-switches or loop bodies.
4. **Nesting & control flow**: Can nested `if`/`for` blocks be flattened using early `return`, `continue`, or `break`? Prefer guard clauses over deep indentation.
5. **Naming**: Are new identifiers clear, consistent with existing conventions, and free of ambiguity? Avoid mixing synonyms (e.g., `tmp` vs `temp`) for the same concept.
6. **Edge cases**: Are zero-length slices, nil maps, and boundary values handled? Does the code distinguish "absent" from "empty"?
7. **Resource cleanup**: For compiler code specifically — are heap temporaries freed on all paths (true and false branches)? Are borrowed vs owned semantics respected?

## Instructions for AI Assistants
- Keep changes minimal and focused; avoid reflowing or reindenting unrelated lines
- Use tabs for indentation (preserve existing indentation style)
Expand Down
12 changes: 12 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ CI: GitHub Actions builds with Go 1.25, installs LLVM 21 + valgrind, and runs `p
- Test/validation command details are optional in commit messages; put full verification details in the PR description when possible.
- PRs: include a clear description, linked issues, unit/E2E tests for changes, and sample before/after output where relevant.

## Code Review Checklist

When reviewing PRs or preparing code for review, check:

1. **Modularity & readability**: Is each function focused on a single responsibility? Can a new reader follow the logic without excessive cross-referencing?
2. **Placement**: Do changes belong in the functions, arguments, and structs they touch, or should logic be moved to a more natural home?
3. **Duplication**: Is there code that duplicates existing patterns in the codebase? Extract shared logic into a helper or utility (e.g., `ast.ExprChildren` for tree-walking) rather than repeating type-switches or loop bodies.
4. **Nesting & control flow**: Can nested `if`/`for` blocks be flattened using early `return`, `continue`, or `break`? Prefer guard clauses over deep indentation.
5. **Naming**: Are new identifiers clear, consistent with existing conventions, and free of ambiguity? Avoid mixing synonyms (e.g., `tmp` vs `temp`) for the same concept.
6. **Edge cases**: Are zero-length slices, nil maps, and boundary values handled? Does the code distinguish "absent" from "empty"?
7. **Resource cleanup**: For compiler code specifically — are heap temporaries freed on all paths (true and false branches)? Are borrowed vs owned semantics respected?

## Instructions for AI Assistants
- Keep changes minimal and focused; avoid reflowing or reindenting unrelated lines.
- Use tabs for indentation (preserve existing indentation style).
Expand Down
29 changes: 29 additions & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,32 @@ func (ce *CallExpression) String() string {

return out.String()
}

// ExprChildren returns the immediate child expressions of an AST node.
// Returns nil for leaf nodes (Identifier, IntegerLiteral, FloatLiteral,
// StringLiteral, HeapStringLiteral).
func ExprChildren(expr Expression) []Expression {
switch e := expr.(type) {
case *InfixExpression:
return []Expression{e.Left, e.Right}
case *PrefixExpression:
return []Expression{e.Right}
case *CallExpression:
return e.Arguments
case *ArrayLiteral:
children := []Expression{}
for _, row := range e.Rows {
children = append(children, row...)
}
return children
case *ArrayRangeExpression:
return []Expression{e.Array, e.Range}
case *RangeLiteral:
children := []Expression{e.Start, e.Stop}
if e.Step != nil {
children = append(children, e.Step)
}
return children
}
return nil
}
86 changes: 86 additions & 0 deletions compiler/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,92 @@ func (c *Compiler) compileArrayScalarInfix(op string, arr *Symbol, scalar *Symbo
return resSym
}

// compileArrayFilter dispatches array filtering to array-array or array-scalar paths.
// Returns a new array containing only LHS elements where the comparison holds.
func (c *Compiler) compileArrayFilter(op string, left *Symbol, right *Symbol, expected Type) *Symbol {
l := c.derefIfPointer(left, "")
r := c.derefIfPointer(right, "")

resArr := expected.(Array)
acc := c.NewArrayAccumulator(resArr)

if l.Type.Kind() == ArrayKind && r.Type.Kind() == ArrayKind {
return c.compileArrayArrayFilter(op, l, r, acc)
}
if l.Type.Kind() == ArrayKind {
return c.compileArrayScalarFilter(op, l, r, acc)
}
// Fallback: LHS is not an array (shouldn't reach here if solver is correct)
return c.compileInfix(op, left, right, expected)
}

// filterPush conditionally appends sym to acc based on cond.
// Emits a branch: if cond is true, push sym; otherwise skip.
func (c *Compiler) filterPush(acc *ArrayAccumulator, sym *Symbol, cond llvm.Value) {
fn := c.builder.GetInsertBlock().Parent()
copyBlock := c.Context.AddBasicBlock(fn, "filter_copy")
nextBlock := c.Context.AddBasicBlock(fn, "filter_next")
c.builder.CreateCondBr(cond, copyBlock, nextBlock)

c.builder.SetInsertPointAtEnd(copyBlock)
c.PushVal(acc, sym)
c.builder.CreateBr(nextBlock)

c.builder.SetInsertPointAtEnd(nextBlock)
}

func (c *Compiler) compileArrayArrayFilter(op string, leftArr *Symbol, rightArr *Symbol, acc *ArrayAccumulator) *Symbol {
leftElem := leftArr.Type.(Array).ColTypes[0]
rightElem := rightArr.Type.(Array).ColTypes[0]

leftLen := c.ArrayLen(leftArr, leftElem)
rightLen := c.ArrayLen(rightArr, rightElem)
minLen := c.builder.CreateSelect(
c.builder.CreateICmp(llvm.IntULT, leftLen, rightLen, "cmp_len"),
leftLen, rightLen, "min_len",
)

r := c.rangeZeroToN(minLen)
c.createLoop(r, func(iter llvm.Value) {
leftVal := c.ArrayGetBorrowed(leftArr, leftElem, iter)
rightVal := c.ArrayGetBorrowed(rightArr, rightElem, iter)

leftSym := &Symbol{Val: leftVal, Type: leftElem}
rightSym := &Symbol{Val: rightVal, Type: rightElem}

cmpResult := defaultOps[opKey{
Operator: op,
LeftType: opType(leftSym.Type.Key()),
RightType: opType(rightSym.Type.Key()),
}](c, leftSym, rightSym, true)

c.filterPush(acc, leftSym, cmpResult.Val)
})

return c.ArrayAccResult(acc)
}

func (c *Compiler) compileArrayScalarFilter(op string, arr *Symbol, scalar *Symbol, acc *ArrayAccumulator) *Symbol {
arrElem := arr.Type.(Array).ColTypes[0]
lenVal := c.ArrayLen(arr, arrElem)

r := c.rangeZeroToN(lenVal)
c.createLoop(r, func(iter llvm.Value) {
val := c.ArrayGetBorrowed(arr, arrElem, iter)
elemSym := &Symbol{Val: val, Type: arrElem}

cmpResult := defaultOps[opKey{
Operator: op,
LeftType: opType(elemSym.Type.Key()),
RightType: opType(scalar.Type.Key()),
}](c, elemSym, scalar, true)

c.filterPush(acc, elemSym, cmpResult.Val)
})

return c.ArrayAccResult(acc)
}

func (c *Compiler) compileArrayUnaryPrefix(op string, arr *Symbol, result Array) *Symbol {
arrType := arr.Type.(Array)
elem := arrType.ColTypes[0]
Expand Down
63 changes: 11 additions & 52 deletions compiler/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,69 +72,28 @@ func NewCFG(sc *ScriptCompiler, cc *CodeCompiler) *CFG {
// collectReads walks an expression tree and returns a slice of all
// the identifier names it finds, put in VarEvent. This is a read-only analysis.
func (cfg *CFG) collectReads(expr ast.Expression) []VarEvent {
// Leaf cases with special handling
switch e := expr.(type) {
// Base cases that do NOT contain identifiers.
// We do nothing and let the function return the initial nil slice.
case *ast.IntegerLiteral, *ast.FloatLiteral:
return nil

case *ast.StringLiteral:
// Collect any identifiers within the format string.
return cfg.collectStringReads(e.Value, e.Token)

case *ast.HeapStringLiteral:
// Collect any identifiers within the format string.
return cfg.collectStringReads(e.Value, e.Token)

case *ast.RangeLiteral:
// a range literal “start:stop[:step]” reads start, stop, and optionally step
evs := cfg.collectReads(e.Start)
evs = append(cfg.collectReads(e.Stop), evs...)
if e.Step != nil {
evs = append(cfg.collectReads(e.Step), evs...)
}
return evs
// Base case that IS an identifier.
case *ast.Identifier:
// Return a new slice
return []VarEvent{{Name: e.Value, Kind: Read, Token: e.Tok()}}
}

// Recursive cases: These nodes contain other expressions.
case *ast.PrefixExpression:
// The result is whatever we find in the right-hand side.
return cfg.collectReads(e.Right)

case *ast.InfixExpression:
leftEvents := cfg.collectReads(e.Left)
rightEvents := cfg.collectReads(e.Right)
// Efficiently append the non-nil slices.
return append(leftEvents, rightEvents...)

case *ast.CallExpression:
var evs []VarEvent // Declares a nil slice
for _, arg := range e.Arguments {
evs = append(evs, cfg.collectReads(arg)...)
}
return evs

case *ast.ArrayLiteral:
// Collect reads from every cell expression in all rows
var evs []VarEvent
for _, row := range e.Rows {
for _, cell := range row {
evs = append(evs, cfg.collectReads(cell)...)
}
}
return evs

case *ast.ArrayRangeExpression:
evs := cfg.collectReads(e.Array)
evs = append(evs, cfg.collectReads(e.Range)...)
return evs

default:
panic(fmt.Sprintf("unhandled expression type: %T", e))
// Recurse into children for composite expressions
children := ast.ExprChildren(expr)
if children == nil {
panic(fmt.Sprintf("unhandled expression type: %T", expr))
}
var evs []VarEvent
for _, child := range children {
evs = append(evs, cfg.collectReads(child)...)
}
return evs
}

func (cfg *CFG) collectStringReads(value string, tok token.Token) []VarEvent {
Expand Down
34 changes: 28 additions & 6 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type Compiler struct {
ExprCache map[ExprKey]*ExprInfo
FuncNameMangled string // current function's mangled name ("" for script level)
Errors []*token.CompileError
condLHS map[ExprKey][]*Symbol // Statement-local: pre-extracted LHS values during cond-expr lowering
}

func NewCompiler(ctx llvm.Context, mangledPath string, cc *CodeCompiler) *Compiler {
Expand Down Expand Up @@ -551,12 +552,22 @@ func (c *Compiler) captureOldValues(idents []*ast.Identifier) []*Symbol {

func (c *Compiler) compileLetStatement(stmt *ast.LetStatement) {
cond, hasConditions := c.compileConditions(stmt)
if !hasConditions {
c.compileAssignments(stmt.Name, stmt.Name, stmt.Value)

// Embedded conditional expressions (comparisons in value position)
// take the most specialized path — they subsume statement conditions.
for _, expr := range stmt.Value {
if c.hasCondExprInTree(expr) {
c.compileCondExprStatement(stmt, cond)
return
}
}

if hasConditions {
c.compileCondStatement(stmt, cond)
return
}

c.compileCondStatement(stmt, cond)
c.compileAssignments(stmt.Name, stmt.Name, stmt.Value)
}

func (c *Compiler) compileExpression(expr ast.Expression, dest []*ast.Identifier) (res []*Symbol) {
Expand Down Expand Up @@ -788,6 +799,13 @@ func (c *Compiler) getRawSymbol(name string) (*Symbol, bool) {

func (c *Compiler) compileInfixExpression(expr *ast.InfixExpression, dest []*ast.Identifier) (res []*Symbol) {
info := c.ExprCache[key(c.FuncNameMangled, expr)]

// Return pre-extracted LHS values for conditional expressions
if c.condLHS != nil {
if lhs, ok := c.condLHS[key(c.FuncNameMangled, expr)]; ok {
return lhs
}
}
// Filter out ranges that are already bound (converted to scalar iterators in outer loops)
pending := c.pendingLoopRanges(info.Ranges)
if len(pending) == 0 {
Expand Down Expand Up @@ -827,8 +845,8 @@ func (c *Compiler) compileInfix(op string, left *Symbol, right *Symbol, expected

return defaultOps[opKey{
Operator: op,
LeftType: l.Type.Key(),
RightType: r.Type.Key(),
LeftType: opType(l.Type.Key()),
RightType: opType(r.Type.Key()),
}](c, l, r, true)
}

Expand All @@ -839,7 +857,11 @@ func (c *Compiler) compileInfixBasic(expr *ast.InfixExpression, info *ExprInfo)
right := c.compileExpression(expr.Right, nil)

for i := 0; i < len(left); i++ {
res = append(res, c.compileInfix(expr.Operator, left[i], right[i], info.OutTypes[i]))
if len(info.CompareModes) > i && info.CompareModes[i] == CondArray {
res = append(res, c.compileArrayFilter(expr.Operator, left[i], right[i], info.OutTypes[i]))
} else {
res = append(res, c.compileInfix(expr.Operator, left[i], right[i], info.OutTypes[i]))
}
}

// Free temporary array operands (literals used in expressions)
Expand Down
Loading