• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

asciimoth / pasta / 26626415405

29 May 2026 08:18AM UTC coverage: 77.855% (-0.3%) from 78.117%
26626415405

push

github

asciimoth
fix(std): fix formular menu handling by some nodes

20 of 72 new or added lines in 12 files covered. (27.78%)

1 existing line in 1 file now uncovered.

5467 of 7022 relevant lines covered (77.86%)

323.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.64
/pasta/std/node_math.go
1
package std
2

3
import (
4
        "slices"
5
        "strconv"
6
        "strings"
7

8
        "github.com/asciimoth/formular"
9
        "github.com/asciimoth/pasta/pasta"
10
)
11

12
type mathNode struct {
13
        pasta.BasicNode
14

15
        op       string
16
        variadic bool
17
        primary  string
18
        result   numberValue
19
        inputs   map[uint64]numberValue
20

21
        w     *pasta.Workspace
22
        id    uint64
23
        out   uint64
24
        lefts []uint64
25
}
26

27
func newMathNode(op string, variadic bool, previous ...*pasta.NodeClassState) *mathNode {
14✔
28
        n := &mathNode{op: op, variadic: variadic, inputs: map[uint64]numberValue{}, result: intValue(0)}
14✔
29
        if state := firstState(previous); state != nil {
17✔
30
                n.primary = state.PrimaryType
3✔
31
        }
3✔
32
        if n.primary != "" {
17✔
33
                n.result = zeroValue(n.primary)
3✔
34
        }
3✔
35
        return n
14✔
36
}
37

38
func (n *mathNode) OnInit(w *pasta.Workspace, _ pasta.Logger, id uint64, _ string, restored *pasta.NodeInitData, _, _, _, isRestored bool) error {
14✔
39
        n.w = w
14✔
40
        n.id = id
14✔
41
        if restored != nil {
28✔
42
                if restored.PrimaryType != "" {
17✔
43
                        n.primary = restored.PrimaryType
3✔
44
                        n.result = zeroValue(n.primary)
3✔
45
                }
3✔
46
                if len(restored.RightPorts) > 0 {
28✔
47
                        n.out = restored.RightPorts[0]
14✔
48
                }
14✔
49
                n.lefts = append([]uint64{}, restored.LeftPorts...)
14✔
50
        }
51
        if n.primary != "" {
17✔
52
                if err := n.w.SetNodePrimary(n.id, n.primary); err != nil {
3✔
53
                        return err
×
54
                }
×
55
                if n.out != 0 {
6✔
56
                        if err := n.w.SetPortTypes(n.out, []string{n.primary}); err != nil {
3✔
57
                                return err
×
58
                        }
×
59
                }
60
        }
61
        n.recalculate(false)
14✔
62
        if err := n.updateLabel(); err != nil {
14✔
63
                return err
×
64
        }
×
65
        n.sendMenuSnapshot()
14✔
66
        if n.variadic && !isRestored {
21✔
67
                n.scheduleBalanceInputs()
7✔
68
        }
7✔
69
        return nil
14✔
70
}
71

72
func (n *mathNode) OnReady() error {
14✔
73
        n.requestAll()
14✔
74
        n.sendAll()
14✔
75
        return nil
14✔
76
}
14✔
77

78
func (n *mathNode) PreLinkAdd(port uint64, linkType, portDirection string) error {
32✔
79
        if linkType != TypeInt && linkType != TypeFloat && linkType != pasta.AnyType {
32✔
NEW
80
                return pasta.LinkTypeErr(linkType)
×
81
        }
×
82
        if portDirection == "left" {
63✔
83
                snapshot, ok := n.w.PortSnapshot(port)
31✔
84
                if ok && len(snapshot.Links) > 0 {
31✔
85
                        return pasta.ErrLinkDup
×
86
                }
×
87
        }
88
        return nil
32✔
89
}
90

91
func (n *mathNode) OnLinkAdd(link, port uint64, linkType, portDirection string) error {
32✔
92
        if portDirection == "left" {
63✔
93
                if n.primary == "" && linkType != pasta.AnyType {
41✔
94
                        if err := n.decidePrimary(linkType); err != nil {
10✔
95
                                return err
×
96
                        }
×
97
                }
98
                if linkType != pasta.AnyType {
61✔
99
                        n.requestLink(link, port, linkType)
30✔
100
                }
30✔
101
                if n.variadic {
54✔
102
                        n.scheduleBalanceInputs()
23✔
103
                }
23✔
104
                return nil
31✔
105
        }
106
        if port == n.out && n.primary != "" {
1✔
107
                n.sendToLink(link)
×
108
        }
×
109
        return nil
1✔
110
}
111

112
func (n *mathNode) OnLinkRemoved(_ uint64, port uint64, _ string, portDirection string) error {
2✔
113
        if portDirection == "left" {
4✔
114
                delete(n.inputs, port)
2✔
115
                n.recalculate(true)
2✔
116
                if n.variadic {
3✔
117
                        n.scheduleBalanceInputs()
1✔
118
                }
1✔
119
        }
120
        return nil
2✔
121
}
122

123
func (n *mathNode) OnPortAdd(port uint64, direction string, _ []string) error {
20✔
124
        if direction == "left" && !slices.Contains(n.lefts, port) {
40✔
125
                n.lefts = append(n.lefts, port)
20✔
126
                n.sortInputs()
20✔
127
        }
20✔
128
        return nil
20✔
129
}
130

131
func (n *mathNode) OnPortRemoved(port uint64, direction string) error {
2✔
132
        if direction == "left" {
4✔
133
                n.lefts = slices.DeleteFunc(n.lefts, func(id uint64) bool { return id == port })
8✔
134
                delete(n.inputs, port)
2✔
135
                n.recalculate(true)
2✔
136
        }
137
        return nil
2✔
138
}
139

140
func (n *mathNode) OnEvent(event pasta.Event, linkType string, _ []string, receiverPortDirection string) error {
79✔
141
        if receiverPortDirection == "right" {
79✔
142
                if (linkType == TypeInt || linkType == TypeFloat) && isValueRequest(event.Payload) {
×
143
                        n.w.SendEvent(pasta.Event{SenderNode: n.id, SenderPort: n.out, ReceiverNode: event.SenderNode, ReceiverPort: event.SenderPort, Payload: n.result.as(linkType).payload()})
×
144
                }
×
145
                return nil
×
146
        }
147
        value, ok := valueFromPayload(linkType, event.Payload)
79✔
148
        if !ok {
80✔
149
                return nil
1✔
150
        }
1✔
151
        if n.primary == "" {
79✔
152
                if err := n.decidePrimary(linkType); err != nil {
1✔
153
                        return err
×
154
                }
×
155
        }
156
        n.inputs[event.ReceiverPort] = value
78✔
157
        n.recalculate(true)
78✔
158
        return nil
78✔
159
}
160

161
func (n *mathNode) decidePrimary(typ string) error {
11✔
162
        n.primary = typ
11✔
163
        n.result = zeroValue(typ)
11✔
164
        if err := n.w.SetNodePrimary(n.id, typ); err != nil {
11✔
165
                return err
×
166
        }
×
167
        if n.out != 0 {
22✔
168
                return n.w.SetPortTypes(n.out, []string{typ})
11✔
169
        }
11✔
170
        return nil
×
171
}
172

173
func (n *mathNode) recalculate(broadcast bool) {
96✔
174
        old := n.result
96✔
175
        typ := n.primary
96✔
176
        if typ == "" {
107✔
177
                typ = TypeInt
11✔
178
        }
11✔
179
        switch n.op {
96✔
180
        case "sub":
8✔
181
                a, b := n.input(0, typ), n.input(1, typ)
8✔
182
                if typ == TypeFloat {
8✔
183
                        n.result = floatValue(a.f - b.f)
×
184
                } else {
8✔
185
                        n.result = intValue(a.i - b.i)
8✔
186
                }
8✔
187
        case "div":
16✔
188
                a, b := n.input(0, typ), n.input(1, typ)
16✔
189
                if typ == TypeFloat {
21✔
190
                        if b.f == 0 {
7✔
191
                                n.result = floatValue(0)
2✔
192
                        } else {
5✔
193
                                n.result = floatValue(a.f / b.f)
3✔
194
                        }
3✔
195
                } else if b.i == 0 {
19✔
196
                        n.result = intValue(0)
8✔
197
                } else {
11✔
198
                        n.result = intValue(a.i / b.i)
3✔
199
                }
3✔
200
        case "mul":
6✔
201
                if typ == TypeFloat {
6✔
202
                        acc := float64(1)
×
203
                        for _, port := range n.lefts {
×
204
                                acc *= n.valueForPort(port, typ).f
×
205
                        }
×
206
                        n.result = floatValue(acc)
×
207
                } else {
6✔
208
                        acc := 1
6✔
209
                        for _, port := range n.lefts {
20✔
210
                                acc *= n.valueForPort(port, typ).i
14✔
211
                        }
14✔
212
                        n.result = intValue(acc)
6✔
213
                }
214
        default:
66✔
215
                if typ == TypeFloat {
76✔
216
                        acc := float64(0)
10✔
217
                        for _, port := range n.lefts {
47✔
218
                                acc += n.valueForPort(port, typ).f
37✔
219
                        }
37✔
220
                        n.result = floatValue(acc)
10✔
221
                } else {
56✔
222
                        acc := 0
56✔
223
                        for _, port := range n.lefts {
195✔
224
                                acc += n.valueForPort(port, typ).i
139✔
225
                        }
139✔
226
                        n.result = intValue(acc)
56✔
227
                }
228
        }
229
        _ = n.updateLabel()
96✔
230
        n.sendMenuBlock()
96✔
231
        if broadcast && old.payload() != n.result.payload() {
134✔
232
                n.sendAll()
38✔
233
        }
38✔
234
}
235

236
func (n *mathNode) input(index int, typ string) numberValue {
48✔
237
        if index >= len(n.lefts) {
48✔
238
                return zeroValue(typ)
×
239
        }
×
240
        return n.valueForPort(n.lefts[index], typ)
48✔
241
}
242

243
func (n *mathNode) valueForPort(port uint64, typ string) numberValue {
238✔
244
        value, ok := n.inputs[port]
238✔
245
        if !ok {
333✔
246
                if n.op == "mul" {
101✔
247
                        return oneValue(typ)
6✔
248
                } else {
95✔
249
                        return zeroValue(typ)
89✔
250
                }
89✔
251
        }
252
        return value.as(typ)
143✔
253
}
254

255
func (n *mathNode) requestAll() {
14✔
256
        for _, port := range n.lefts {
38✔
257
                snapshot, ok := n.w.PortSnapshot(port)
24✔
258
                if !ok {
24✔
259
                        continue
×
260
                }
261
                for _, link := range snapshot.Links {
28✔
262
                        n.requestLink(link, port, "")
4✔
263
                }
4✔
264
        }
265
}
266

267
func (n *mathNode) requestLink(link, port uint64, linkType string) {
34✔
268
        snapshot, ok := n.w.LinkSnapshot(link)
34✔
269
        if !ok {
34✔
270
                return
×
271
        }
×
272
        receiverNode, receiverPort := otherEndpoint(snapshot, port)
34✔
273
        n.w.SendEvent(pasta.Event{SenderNode: n.id, SenderPort: port, ReceiverNode: receiverNode, ReceiverPort: receiverPort, Payload: RequestValue{}})
34✔
274
}
275

276
func (n *mathNode) sendAll() {
52✔
277
        if n.primary == "" {
63✔
278
                return
11✔
279
        }
11✔
280
        port, ok := n.w.PortSnapshot(n.out)
41✔
281
        if !ok {
41✔
282
                return
×
283
        }
×
284
        for _, link := range port.Links {
43✔
285
                n.sendToLink(link)
2✔
286
        }
2✔
287
}
288

289
func (n *mathNode) sendToLink(link uint64) {
2✔
290
        snapshot, ok := n.w.LinkSnapshot(link)
2✔
291
        if !ok {
2✔
292
                return
×
293
        }
×
294
        receiverNode, receiverPort := otherEndpoint(snapshot, n.out)
2✔
295
        n.w.SendEvent(pasta.Event{SenderNode: n.id, SenderPort: n.out, ReceiverNode: receiverNode, ReceiverPort: receiverPort, Payload: n.result.as(snapshot.Type).payload()})
2✔
296
}
297

298
func (n *mathNode) scheduleBalanceInputs() {
31✔
299
        n.w.AddPendingOp(n.balanceInputs)
31✔
300
}
31✔
301

302
func (n *mathNode) balanceInputs() {
31✔
303
        if !n.variadic {
31✔
304
                return
×
305
        }
×
306
        n.refreshLefts()
31✔
307
        free := n.freeInputPorts()
31✔
308
        if len(free) == 0 {
51✔
309
                _, _ = n.w.AddPort(inputPortForNode(n.id, len(n.lefts)+1))
20✔
310
                n.refreshLefts()
20✔
311
        }
20✔
312
        for {
64✔
313
                n.refreshLefts()
33✔
314
                free = n.freeInputPorts()
33✔
315
                if len(free) <= 1 {
64✔
316
                        break
31✔
317
                }
318
                trailing := n.trailingFreePort()
2✔
319
                if trailing == 0 {
2✔
320
                        break
×
321
                }
322
                n.w.RemovePort(trailing)
2✔
323
        }
324
        n.sortInputs()
31✔
325
}
326

327
func (n *mathNode) refreshLefts() {
84✔
328
        snapshot, ok := n.w.NodeSnapshot(n.id)
84✔
329
        if !ok {
84✔
330
                return
×
331
        }
×
332
        n.lefts = append([]uint64{}, snapshot.LeftPorts...)
84✔
333
        n.sortInputs()
84✔
334
}
335

336
func (n *mathNode) freeInputPorts() []uint64 {
64✔
337
        free := []uint64{}
64✔
338
        for _, port := range n.lefts {
196✔
339
                snapshot, ok := n.w.PortSnapshot(port)
132✔
340
                if ok && len(snapshot.Links) == 0 {
180✔
341
                        free = append(free, port)
48✔
342
                }
48✔
343
        }
344
        return free
64✔
345
}
346

347
func (n *mathNode) trailingFreePort() uint64 {
2✔
348
        for i := len(n.lefts) - 1; i >= 0; i-- {
4✔
349
                port := n.lefts[i]
2✔
350
                snapshot, ok := n.w.PortSnapshot(port)
2✔
351
                if ok && len(snapshot.Links) == 0 {
4✔
352
                        return port
2✔
353
                }
2✔
354
                if ok && len(snapshot.Links) > 0 {
×
355
                        return 0
×
356
                }
×
357
        }
358
        return 0
×
359
}
360

361
func (n *mathNode) sortInputs() {
135✔
362
        slices.SortFunc(n.lefts, func(a, b uint64) int {
318✔
363
                return inputIndex(n.w, a) - inputIndex(n.w, b)
183✔
364
        })
183✔
365
        _ = n.w.SetNodePortOrder(n.id, "left", n.lefts)
135✔
366
}
367

368
func (n *mathNode) updateLabel() error {
110✔
369
        return n.w.SetNodeLabel(n.id, n.result.label())
110✔
370
}
110✔
371

372
func (n *mathNode) sendMenuSnapshot() {
14✔
373
        n.w.SendNodeMenuMsg(n.id, formular.MenuSnapshotMessage{
14✔
374
                MessageBase: formular.MessageBase{Type: formular.MessageMenuSnapshot, MenuID: pasta.NodeMenuID(n.id), MenuGeneration: 1},
14✔
375
                Blocks:      []formular.Block{n.menuBlock()},
14✔
376
        })
14✔
377
}
14✔
378

379
func (n *mathNode) sendMenuBlock() {
96✔
380
        if n.w == nil || n.id == 0 {
96✔
381
                return
×
382
        }
×
383
        n.w.SendNodeMenuMsg(n.id, formular.BlockSnapshotMessage{
96✔
384
                MessageBase: formular.MessageBase{Type: formular.MessageBlockSnapshot, MenuID: pasta.NodeMenuID(n.id), MenuGeneration: 1, BlockGeneration: 1},
96✔
385
                Block:       n.menuBlock(),
96✔
386
        })
96✔
387
}
388

389
func (n *mathNode) menuBlock() formular.Block {
110✔
390
        return formular.Block{
110✔
391
                ID: "state", Order: 10, Generation: 1,
110✔
392
                Items: []formular.Item{{Type: formular.ItemField, ID: "value", Label: "Value", Field: &formular.Field{Kind: menuFieldKind(n.result.typ), Value: n.result.menuValue(), Readonly: true}}},
110✔
393
        }
110✔
394
}
110✔
395

396
func inputPortForNode(node uint64, index int) pasta.Port {
20✔
397
        port := inputPort(index)
20✔
398
        port.Node = node
20✔
399
        return port
20✔
400
}
20✔
401

402
func inputIndex(w *pasta.Workspace, port uint64) int {
438✔
403
        snapshot, ok := w.PortSnapshot(port)
438✔
404
        if !ok {
438✔
405
                return 0
×
406
        }
×
407
        _, raw, ok := strings.Cut(snapshot.Name, "input ")
438✔
408
        if !ok {
438✔
409
                return 0
×
410
        }
×
411
        index, _ := strconv.Atoi(raw)
438✔
412
        return index
438✔
413
}
414

415
func otherEndpoint(link pasta.LinkSnapshot, port uint64) (uint64, uint64) {
225✔
416
        if link.LeftPort == port {
327✔
417
                return link.RightPortNode, link.RightPort
102✔
418
        }
102✔
419
        return link.LeftPortNode, link.LeftPort
123✔
420
}
421

422
func isValueRequest(payload any) bool {
79✔
423
        _, ok := payload.(RequestValue)
79✔
424
        return ok
79✔
425
}
79✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc