Skip to content

Commit 048d812

Browse files
committed
feat(runtime): schedule object fields concurrently
Evaluate object literals as dependency graphs so independent fields can run in parallel while dependent fields wait for their prerequisites. Detect cyclic field dependencies during inference and keep publication deterministic. Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
1 parent 4cb1c12 commit 048d812

6 files changed

Lines changed: 288 additions & 35 deletions

File tree

pkg/dang/block.go

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -495,10 +495,20 @@ func (o *Object) Infer(ctx context.Context, env hm.Env, fresh hm.Fresher) (hm.Ty
495495
primary: mod,
496496
lexical: env.(Env),
497497
}
498-
for _, slot := range o.Slots {
499-
_, err := slot.Infer(ctx, inferEnv, fresh)
500-
if err != nil {
501-
return nil, err
498+
graph, err := buildObjectSlotGraph(o.Slots)
499+
if err != nil {
500+
return nil, NewInferError(err, o)
501+
}
502+
layers, err := graph.Layers()
503+
if err != nil {
504+
return nil, NewInferError(err, o)
505+
}
506+
for _, layer := range layers {
507+
for _, slotIdx := range layer {
508+
_, err := o.Slots[slotIdx].Infer(ctx, inferEnv, fresh)
509+
if err != nil {
510+
return nil, err
511+
}
502512
}
503513
}
504514
o.Mod = mod
@@ -513,10 +523,65 @@ func (o *Object) Eval(ctx context.Context, env EvalEnv) (Value, error) {
513523
}
514524
newMod := NewModuleValue(o.Mod)
515525
evalEnv := CreateCompositeEnv(newMod, env)
516-
for _, slot := range o.Slots {
517-
_, err := EvalNode(ctx, evalEnv, slot)
518-
if err != nil {
519-
return nil, err
526+
graph, err := buildObjectSlotGraph(o.Slots)
527+
if err != nil {
528+
return nil, err
529+
}
530+
layers, err := graph.Layers()
531+
if err != nil {
532+
return nil, err
533+
}
534+
535+
evalCtx, cancel := context.WithCancel(ctx)
536+
defer cancel()
537+
for _, layer := range layers {
538+
type slotResult struct {
539+
idx int
540+
val Value
541+
err error
542+
}
543+
results := make(chan slotResult, len(layer))
544+
for _, slotIdx := range layer {
545+
slot := o.Slots[slotIdx]
546+
go func(idx int, slot *SlotDecl) {
547+
// Evaluate each slot against a fork of the object environment. The
548+
// fork can read already-published dependency fields through its
549+
// parent, but any incidental local writes remain private to this
550+
// slot. The object itself is only mutated below, after the whole
551+
// layer has completed.
552+
slotEnv := CreateCompositeEnv(newMod.Fork(), env)
553+
val, err := WithEvalErrorHandling(evalCtx, slot, func() (Value, error) {
554+
return slot.EvalValue(evalCtx, slotEnv)
555+
})
556+
if err != nil {
557+
err = fmt.Errorf("evaluating object field %q: %w", slot.Name.Name, err)
558+
}
559+
results <- slotResult{idx: idx, val: val, err: err}
560+
}(slotIdx, slot)
561+
}
562+
563+
values := make(map[int]Value, len(layer))
564+
var firstErr error
565+
firstErrIdx := len(o.Slots)
566+
for range layer {
567+
res := <-results
568+
if res.err != nil {
569+
cancel()
570+
if firstErr == nil || res.idx < firstErrIdx {
571+
firstErr = res.err
572+
firstErrIdx = res.idx
573+
}
574+
continue
575+
}
576+
values[res.idx] = res.val
577+
}
578+
if firstErr != nil {
579+
return nil, firstErr
580+
}
581+
582+
// Publish completed fields in source order for deterministic object layout.
583+
for _, slotIdx := range layer {
584+
o.Slots[slotIdx].Publish(evalEnv, values[slotIdx])
520585
}
521586
}
522587
return newMod, nil

pkg/dang/object_graph.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package dang
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
type objectSlotGraph struct {
9+
slots []*SlotDecl
10+
deps map[int]map[int]struct{}
11+
rdeps map[int]map[int]struct{}
12+
names []string
13+
}
14+
15+
func buildObjectSlotGraph(slots []*SlotDecl) (*objectSlotGraph, error) {
16+
localNames := make(map[string]int, len(slots))
17+
names := make([]string, len(slots))
18+
for i, slot := range slots {
19+
declared := slot.DeclaredSymbols()
20+
if len(declared) != 1 {
21+
return nil, fmt.Errorf("object slot must declare exactly one name")
22+
}
23+
name := declared[0]
24+
if prev, ok := localNames[name]; ok {
25+
return nil, fmt.Errorf("object literal has duplicate field %q (previous declaration at field %d)", name, prev+1)
26+
}
27+
localNames[name] = i
28+
names[i] = name
29+
}
30+
31+
g := &objectSlotGraph{
32+
slots: slots,
33+
deps: make(map[int]map[int]struct{}, len(slots)),
34+
rdeps: make(map[int]map[int]struct{}, len(slots)),
35+
names: names,
36+
}
37+
for i, slot := range slots {
38+
for _, ref := range slot.ReferencedSymbols() {
39+
dep, ok := localNames[ref]
40+
if !ok || dep == i {
41+
continue
42+
}
43+
if g.deps[i] == nil {
44+
g.deps[i] = map[int]struct{}{}
45+
}
46+
if g.rdeps[dep] == nil {
47+
g.rdeps[dep] = map[int]struct{}{}
48+
}
49+
g.deps[i][dep] = struct{}{}
50+
g.rdeps[dep][i] = struct{}{}
51+
}
52+
}
53+
return g, nil
54+
}
55+
56+
func (g *objectSlotGraph) Layers() ([][]int, error) {
57+
remaining := make(map[int]map[int]struct{}, len(g.slots))
58+
ready := []int{}
59+
for i := range g.slots {
60+
remaining[i] = make(map[int]struct{}, len(g.deps[i]))
61+
for dep := range g.deps[i] {
62+
remaining[i][dep] = struct{}{}
63+
}
64+
if len(remaining[i]) == 0 {
65+
ready = append(ready, i)
66+
}
67+
}
68+
69+
var layers [][]int
70+
done := make(map[int]struct{}, len(g.slots))
71+
for len(ready) > 0 {
72+
layer := append([]int(nil), ready...)
73+
layers = append(layers, layer)
74+
ready = nil
75+
for _, i := range layer {
76+
done[i] = struct{}{}
77+
for dependent := range g.rdeps[i] {
78+
delete(remaining[dependent], i)
79+
if len(remaining[dependent]) == 0 {
80+
if _, alreadyDone := done[dependent]; !alreadyDone {
81+
ready = append(ready, dependent)
82+
}
83+
}
84+
}
85+
}
86+
}
87+
88+
if len(done) != len(g.slots) {
89+
return nil, fmt.Errorf("object literal has cyclic field dependencies: %s", g.cycleString(done))
90+
}
91+
return layers, nil
92+
}
93+
94+
func (g *objectSlotGraph) cycleString(done map[int]struct{}) string {
95+
visiting := map[int]bool{}
96+
visited := map[int]bool{}
97+
var stack []int
98+
var cycle []int
99+
var dfs func(int) bool
100+
dfs = func(i int) bool {
101+
visiting[i] = true
102+
stack = append(stack, i)
103+
for dep := range g.deps[i] {
104+
if _, ok := done[dep]; ok {
105+
continue
106+
}
107+
if visiting[dep] {
108+
start := 0
109+
for start < len(stack) && stack[start] != dep {
110+
start++
111+
}
112+
cycle = append(append([]int(nil), stack[start:]...), dep)
113+
return true
114+
}
115+
if !visited[dep] && dfs(dep) {
116+
return true
117+
}
118+
}
119+
stack = stack[:len(stack)-1]
120+
visiting[i] = false
121+
visited[i] = true
122+
return false
123+
}
124+
for i := range g.slots {
125+
if _, ok := done[i]; ok || visited[i] {
126+
continue
127+
}
128+
if dfs(i) {
129+
break
130+
}
131+
}
132+
if len(cycle) == 0 {
133+
var names []string
134+
for i := range g.slots {
135+
if _, ok := done[i]; !ok {
136+
names = append(names, g.names[i])
137+
}
138+
}
139+
return strings.Join(names, ", ")
140+
}
141+
parts := make([]string, len(cycle))
142+
for i, idx := range cycle {
143+
parts[i] = g.names[idx]
144+
}
145+
return strings.Join(parts, " -> ")
146+
}

pkg/dang/slots.go

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -175,40 +175,48 @@ func (s *SlotDecl) Infer(ctx context.Context, env hm.Env, fresh hm.Fresher) (hm.
175175
return definedType, nil
176176
}
177177

178-
func (s *SlotDecl) Eval(ctx context.Context, env EvalEnv) (Value, error) {
179-
return WithEvalErrorHandling(ctx, s, func() (Value, error) {
180-
val, defined := env.GetLocal(s.Name.Name)
181-
if defined {
182-
// already defined (e.g. through constructor), nothing to do
183-
return val, nil
184-
}
178+
func (s *SlotDecl) EvalValue(ctx context.Context, env EvalEnv) (Value, error) {
179+
val, defined := env.GetLocal(s.Name.Name)
180+
if defined {
181+
// already defined (e.g. through constructor), nothing to do
182+
return val, nil
183+
}
185184

186-
if s.Value == nil {
187-
// Check if this is a required (non-null) type without a value
188-
// This is a runtime error - required types must have values
189-
if inferredType := s.GetInferredType(); inferredType != nil {
190-
if _, isNonNull := inferredType.(hm.NonNullType); isNonNull {
191-
return nil, fmt.Errorf("required slot %q (type %s) has no value", s.Name.Name, inferredType.Name())
192-
}
185+
if s.Value == nil {
186+
// Check if this is a required (non-null) type without a value
187+
// This is a runtime error - required types must have values
188+
if inferredType := s.GetInferredType(); inferredType != nil {
189+
if _, isNonNull := inferredType.(hm.NonNullType); isNonNull {
190+
return nil, fmt.Errorf("required slot %q (type %s) has no value", s.Name.Name, inferredType.Name())
193191
}
194-
195-
// If no value is provided, this is just a type declaration
196-
// Add a null value to the environment as a placeholder
197-
env.SetWithVisibility(s.Name.Name, NullValue{}, s.Visibility)
198-
return NullValue{}, nil
199192
}
200193

201-
// Evaluate the value expression with proper error context
202-
val, err := EvalNode(ctx, env, s.Value)
194+
// If no value is provided, this is just a type declaration
195+
// Add a null value to the environment as a placeholder
196+
return NullValue{}, nil
197+
}
198+
199+
// Evaluate the value expression with proper error context
200+
val, err := EvalNode(ctx, env, s.Value)
201+
if err != nil {
202+
// Convert error with proper source location from the failing node
203+
return nil, err
204+
}
205+
206+
return val, nil
207+
}
208+
209+
func (s *SlotDecl) Publish(env EvalEnv, val Value) {
210+
env.SetWithVisibility(s.Name.Name, val, s.Visibility)
211+
}
212+
213+
func (s *SlotDecl) Eval(ctx context.Context, env EvalEnv) (Value, error) {
214+
return WithEvalErrorHandling(ctx, s, func() (Value, error) {
215+
val, err := s.EvalValue(ctx, env)
203216
if err != nil {
204-
// Convert error with proper source location from the failing node
205217
return nil, err
206218
}
207-
208-
// Add the value to the environment for future use
209-
// If it's a ModuleValue, use SetWithVisibility to track visibility
210-
env.SetWithVisibility(s.Name.Name, val, s.Visibility)
211-
219+
s.Publish(env, val)
212220
return val, nil
213221
})
214222
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pub bad = {{
2+
a: b
3+
b: a
4+
}}
5+
6+
bad

tests/test_object_concurrency.dang

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
pub obj = {{
2+
a: 1,
3+
b: 2,
4+
c: a + b,
5+
base: "hello",
6+
derived: base + " world",
7+
independent: "!"
8+
}}
9+
10+
assert { obj.c == 3 }
11+
assert { obj.derived == "hello world" }
12+
assert { obj.independent == "!" }
13+
14+
pub forward = {{
15+
derived: base + " world",
16+
base: "hello"
17+
}}
18+
19+
assert { forward.derived == "hello world" }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Error: object literal has cyclic field dependencies: a -> b -> a
2+
--> errors/object_field_cycle.dang:1:11
3+
 |
4+
 1 | pub bad = {{
5+
 ^^
6+
 2 | a: b
7+
 3 | b: a
8+
 |
9+

0 commit comments

Comments
 (0)