Skip to content

Commit 14ae69b

Browse files
authored
Merge pull request #2 from lf-lang/refine
Improve clarify of the tutorial materials and add CI test.
2 parents 2342038 + eb0c0a0 commit 14ae69b

14 files changed

Lines changed: 159 additions & 75 deletions

.github/workflows/lf-ci.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- "releases/*"
8+
pull_request:
9+
workflow_dispatch:
10+
11+
jobs:
12+
check-compile-and-run:
13+
name: Compile and run LF examples
14+
runs-on: ubuntu-24.04
15+
16+
steps:
17+
- name: Check out repository
18+
uses: actions/checkout@v4
19+
with:
20+
submodules: recursive
21+
22+
- name: Set up Java 17
23+
run: |
24+
echo "$JAVA_HOME_17_X64/bin" >> "$GITHUB_PATH"
25+
echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
26+
shell: bash
27+
28+
- name: Install C build dependencies
29+
run: |
30+
sudo apt-get update
31+
sudo apt-get install -y build-essential cmake
32+
33+
- name: Check LF files compile
34+
uses: lf-lang/action-check-lf-files@main
35+
with:
36+
check_mode: "compile"
37+
no_compile_flag: false
38+
search_dir: "src"
39+
compiler_ref: "master"
40+
41+
- name: Smoke-run generated LF programs
42+
shell: bash
43+
run: |
44+
set -euo pipefail
45+
46+
shopt -s nullglob
47+
lf_files=(src/*.lf)
48+
49+
if (( ${#lf_files[@]} == 0 )); then
50+
echo "No .lf files found under src/."
51+
exit 1
52+
fi
53+
54+
for file in "${lf_files[@]}"; do
55+
name="$(basename "$file" .lf)"
56+
57+
launch_script="bin/${name}_launch.sh"
58+
executable="bin/${name}"
59+
60+
if [[ -x "$launch_script" ]]; then
61+
runner="$launch_script"
62+
elif [[ -x "$executable" ]]; then
63+
runner="$executable"
64+
else
65+
echo "No generated executable or launch script found for $file."
66+
exit 1
67+
fi
68+
69+
echo "::group::Run $runner"
70+
set +e
71+
timeout --kill-after=5s 10s "$runner"
72+
status=$?
73+
set -e
74+
75+
if [[ "$status" -ne 0 && "$status" -ne 124 && "$status" -ne 137 ]]; then
76+
echo "$runner failed with exit code $status."
77+
exit "$status"
78+
fi
79+
80+
if [[ "$status" -eq 124 || "$status" -eq 137 ]]; then
81+
echo "$runner ran until the smoke-test timeout, as expected for a reactive example."
82+
fi
83+
echo "::endgroup::"
84+
done

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bin/*
2+
fed-gen/*

01-actor-model.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Step 1: The Actor Model Commutative Grid Dispatch
1+
# Step 1: The Actor Model: Commutative Grid Dispatch
22

33
## The Problem We're Solving
44

5-
Two control centersone in California, one in New York each manage a portion of the national grid. Both maintain a copy of the **grid balance**: a signed integer representing net generation in megawatts (positive = excess, negative = deficit).
5+
Two control centers, one in California and one in New York, each manage a portion of the national grid. Both maintain a copy of the **grid balance**: a signed integer representing net generation in megawatts (positive = excess, negative = deficit).
66

77
Operators at either location can issue dispatch commands:
88
- **Dispatch up**: bring more generation online (+MW)
@@ -29,7 +29,7 @@ The squiggly arrows (`~>`) are **physical connections** in Lingua Franca: they u
2929

3030
## The Code
3131

32-
See [`src/DistibutedPowerGrid1_Actor.lf`](src/DistibutedPowerGrid1_Actor.lf).
32+
See [`src/Step1_Actor.lf`](src/Step1_Actor.lf).
3333

3434
The core reactor is `SimpleGridManager`:
3535

@@ -80,9 +80,9 @@ Each grid manager receives commands from **both** operators and keeps its own co
8080

8181
---
8282

83-
## Why This Works Sometimes
83+
## Why This Works, Sometimes
8484

85-
The operation `balance += value` has a special mathematical property: it is **associative and commutative**. It doesn't matter what order the additions happen the final sum is always the same.
85+
The operation `balance += value` has a special mathematical property: it is **associative and commutative**. It doesn't matter what order the additions happen; the final sum is always the same.
8686

8787
This means that even though `gm1` and `gm2` may process the same two commands in different orders, they will eventually agree on the same balance. This property is called **eventual consistency**.
8888

@@ -92,15 +92,15 @@ More formally, this design satisfies **ACID 2.0** properties (Helland & Campbell
9292
- **I**dempotent: TCP guarantees exactly-once delivery, so each command is applied exactly once
9393
- **D**istributed: state is maintained at multiple nodes
9494

95-
A datatype with these properties is called a **Conflict-Free Replicated Datatype (CRDT)** one of the simplest CRDTs in existence.
95+
A datatype with these properties is called a **Conflict-Free Replicated Datatype (CRDT)**. This example is one of the simplest CRDTs in existence.
9696

9797
---
9898

9999
## The Catch
100100

101-
This design would allow operators to curtail generation far below zero a dangerous grid imbalance that could trip protective relays and cause a cascading blackout.
101+
This design would allow operators to curtail generation far below zero, creating a dangerous grid imbalance that could trip protective relays and cause a cascading blackout.
102102

103-
Any **business logic** that enforces limits (e.g., "don't curtail if balance is already at its minimum threshold") breaks commutativity — and with it, our consistency guarantees.
103+
Any **business logic** that enforces limits (e.g., "don't curtail if balance is already at its minimum threshold") breaks commutativity. That also breaks the consistency guarantee from this simple CRDT-style design.
104104

105105
That's what we explore next.
106106

@@ -112,8 +112,8 @@ That's what we explore next.
112112

113113
2. What would happen if TCP delivery were *not* guaranteed? How would the ACID 2.0 / CRDT properties need to change?
114114

115-
3. Why does the CAL theorem (Consistency vs. Availability under Latency constraints) apply here? What is the partial order? [[More Reading]](https://arxiv.org/abs/2301.08906)
115+
3. Now consider the potential effect of network delays in this example. How would network delays affect the consistency of this example?
116116

117117
---
118118

119-
**Next:** [Step 2 When Operations Are Non-Commutative](02-inconsistency.md)
119+
**Next:** [Step 2: When Operations Are Non-Commutative](02-inconsistency.md)

02-inconsistency.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Step 2: When Operations Are Non-Commutative The Consistency Problem
1+
# Step 2: When Operations Are Non-Commutative: The Consistency Problem
22

33
## Adding Real Business Logic
44

@@ -10,7 +10,7 @@ Real grid operators enforce **safety constraints**. A typical rule:
1010
>
1111
> If a curtailment would push the balance below the threshold, reject it and log an **imbalance event** (which triggers automated protective relays in a real system).
1212
13-
Let's say our minimum safe threshold is **−200 MW**. Here is the updated reactor (See [`src/DistibutedPowerGrid2_Inconsistency.lf`](src/DistibutedPowerGrid2_Inconsistency.lf)).
13+
Let's say our minimum safe threshold is **−200 MW**. Here is the updated reactor (See [`src/Step2_Inconsistency.lf`](src/Step2_Inconsistency.lf)).
1414

1515

1616
```lf
@@ -59,17 +59,17 @@ This new operation is **not commutative**. The result of applying two commands d
5959
- California: curtail −80 MW (would bring balance to −230 MW, below threshold)
6060
- New York: dispatch +100 MW (would bring balance to −50 MW)
6161

62-
**Scenario A California manager sees curtail first:**
62+
**Scenario A: California manager sees curtail first:**
6363
1. California curtail (−80): `−150 + (−80) = −230`. Below threshold → **rejected**.
6464
2. New York dispatch (+100): `−150 + 100 = −50`. Applied.
6565
3. Final balance at `gm1`: **−50 MW**. No imbalance event.
6666

67-
**Scenario B California manager sees dispatch first:**
67+
**Scenario B: California manager sees dispatch first:**
6868
1. New York dispatch (+100): `−150 + 100 = −50`. Applied.
6969
2. California curtail (−80): `−50 + (−80) = −130`. Above threshold → **applied**.
7070
3. Final balance at `gm1`: **−130 MW**. No imbalance event.
7171

72-
Since `gm1` and `gm2` receive these messages over physical (unordered) connections, they may each experience a different scenario. **They permanently disagree on the balance** and worse, they may disagree on whether an imbalance event occurred.
72+
Since `gm1` and `gm2` receive these messages over physical (unordered) connections, they may each experience a different scenario. **They permanently disagree on the balance**, and worse, they may disagree on whether an imbalance event occurred.
7373

7474
This is the fundamental consistency problem in distributed systems.
7575

@@ -90,14 +90,14 @@ New York node: dispatch +100 ───────────
9090
9191
gm2 final: -50 MW ✓ no event
9292
93-
But gm1 (-130) ≠ gm2 (-50) INCONSISTENT STATE!
93+
But gm1 (-130) ≠ gm2 (-50): INCONSISTENT STATE!
9494
```
9595

9696
In a real grid, this inconsistency means the two control centers have **contradictory views of grid health**. Automated systems making decisions based on these views could take opposing corrective actions, worsening the situation.
9797

9898
---
9999

100-
## Fixing It The Options
100+
## Fixing It: The Options
101101

102102
We'll explore three approaches to fix the inconsistency issue:
103103

@@ -121,4 +121,4 @@ The single-node approach defeats the purpose of having two control centers. So w
121121

122122
---
123123

124-
**Next:** [Step 3 Adding Logical Timestamps](03-timestamps.md)
124+
**Next:** [Step 3: Adding Logical Timestamps](03-timestamps.md)

03-timestamps.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
# Step 3: Logical Timestamps Ordering Events Across the Grid
1+
# Step 3: Logical Timestamps: Ordering Events Across the Grid
22

33
## The Core Insight
44

5-
When a California operator curtails generation at the same physical instant that a New York operator dispatches generation, there is no objective "ground truth" about which happened first. Special relativity tells us the ordering can depend on the observer and for events separated by thousands of kilometers, the difference in light travel time is measurable.
5+
When a California operator curtails generation at the same physical instant that a New York operator dispatches generation, there is no objective "ground truth" about which happened first. Special relativity tells us the ordering can depend on the observer, and for events separated by thousands of kilometers, the difference in light travel time is measurable.
66

7-
Fortunately, we don't need ground truth. We just need **both control nodes to agree on the same ordering** and for that ordering to look reasonable to human operators. Logical timestamps achieve exactly this.
7+
Fortunately, we don't need ground truth. We need **both control nodes to agree on the same ordering**, and for that ordering to look reasonable to human operators. Logical timestamps achieve exactly this.
88

99
---
1010

1111
## What Is Logical Time?
1212

13-
Lingua Franca assigns a **timestamp** to every message at the point it is created. In a federated (distributed) program, each node uses its **local physical clock** to assign timestamps. Clock synchronization protocols like NTP, PTP, or GPS ensure that these clocks are close to each other within a bounded error `ε`.
13+
Lingua Franca assigns a **timestamp** to every message at the point it is created. In a federated (distributed) program, each node uses its **local physical clock** to assign timestamps. Clock synchronization protocols like NTP, PTP, or GPS keep these clocks close to each other, within a bounded error `ε`.
1414

1515
A timestamp becomes a **logical time** because:
16-
- When Node A sends a message with timestamp `t`, Node B processes it at logical time `t` even if Node B's physical clock has already advanced past `t`.
16+
- When Node A sends a message with timestamp `t`, Node B processes it at logical time `t`, even if Node B's physical clock has already advanced past `t`.
1717
- Logical time is a shared reference frame that both nodes agree on, independent of physical clock drift.
1818

1919
---
@@ -38,21 +38,21 @@ op1.command -> gm1.in1
3838
op1.command -> gm2.in1
3939
```
4040

41-
This small change has a profound effect: LF now **guarantees** that both `gm1` and `gm2` handle every pair of commands in the same logical time order.
41+
This small change has a profound effect: LF now gives both `gm1` and `gm2` the same logical timestamps and a deterministic rule for processing them, provided the coordination assumptions are met.
4242

4343
---
4444

4545
## Code
4646

47-
See [`src/DistibutedPowerGrid3_TimeStamped.lf`](src/DistibutedPowerGrid3_TimeStamped.lf). And here is what our system looks like:
47+
See [`src/Step3_Timestamps.lf`](src/Step3_Timestamps.lf). And here is what our system looks like:
4848

4949
![DistributedPowerGrid3 TimeStamped](fig/DistibutedPowerGrid3_TimeStamped.svg)
5050

5151

5252
Key differences from Step 2:
5353
1. Connections use `->` instead of `~>`
5454
2. `GridManager` has a `maxwait` attribute
55-
3. No other changes to the reactor logic timestamp ordering is handled by the LF runtime
55+
3. No other changes to the reactor logic; timestamp ordering is handled by the LF runtime
5656

5757
---
5858

@@ -73,7 +73,7 @@ reaction(in1, in2) -> out {=
7373
=}
7474
```
7575

76-
Because the connection wiring is symmetric (California always feeds `in1`, New York always feeds `in2` at both managers), both managers give priority to the California operator at simultaneous timestamps. This is a deterministic policynot the most fair one, but a consistent one.
76+
Because the connection wiring is symmetric (California always feeds `in1`, New York always feeds `in2` at both managers), both managers give priority to the California operator at simultaneous timestamps. This is a deterministic policy. It may not be the fairest policy, but it is consistent.
7777

7878
---
7979

@@ -151,11 +151,11 @@ If it is OK for these reports to be made late, then we can annotate the reaction
151151

152152
## The CAL Theorem
153153

154-
The waiting introduced by timestamps is not a bug it is **fundamental**. The **CAL theorem** ([Lee et al., 2023](https://doi.org/10.1145/3609119)) states:
154+
The waiting introduced by timestamps is not a bug; it is **fundamental**. The **CAL theorem** ([Lee et al., 2023](https://doi.org/10.1145/3609119)) states:
155155

156156
> It is impossible to achieve consistency without paying a price in **availability**, where the price is proportional to the latencies in the system.
157157
158-
"Availability" here means: how long must an operator wait before their dispatch command takes effect? The longer the `maxwait`, the more consistent the system — and the longer operators wait. We'll return to this in Step 6.
158+
"Availability" here means: how long must an operator wait before their dispatch command takes effect? A larger `maxwait` can tolerate larger apparent latency, but operators may wait longer. We'll return to this in Step 6.
159159

160160
---
161161

@@ -169,4 +169,4 @@ The waiting introduced by timestamps is not a bug — it is **fundamental**. The
169169

170170
---
171171

172-
**Next:** [Step 4 Conservative Coordination with Null Messages](04-conservative.md)
172+
**Next:** [Step 4: Conservative Coordination with Null Messages](04-conservative.md)

04-conservative.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Step 4: Conservative Coordination Chandy-Misra with Null Messages
1+
# Step 4: Conservative Coordination: Chandy-Misra with Null Messages
22

33
## The Problem with Finite maxwait
44

@@ -33,17 +33,17 @@ LF supports this method via `maxwait = forever`:
3333
gm1 = new GridManager()
3434
```
3535

36-
With `maxwait = forever`, the grid manager will wait **indefinitely** until it receives positive evidence from the remote node that no message with tag ≤ `g` is coming.
36+
With `maxwait = forever`, the grid manager will wait **indefinitely** until it receives positive evidence from the remote node that no message with tag ≤ `g` is coming.
3737

3838
Of course, waiting forever sounds like a bad idea.
39-
What if the remote `GridInterface`
40-
How does that evidence arrive? That's where **null messages** come in.
39+
What if the remote `GridInterface` has no real commands to send?
40+
How does the evidence arrive? That's where **null messages** come in.
4141

4242
---
4343

4444
## Null Messages
4545

46-
In our grid, commands are sent only when operators issue dispatches or curtailments. If California issues no commands for 10 minutes, New York's manager has no evidence about California's timeline and blocks forever under `maxwait = forever`.
46+
In our grid, commands are sent only when operators issue dispatches or curtailments. If California issues no commands for 10 minutes, New York's manager has no evidence about California's timeline and blocks forever under `maxwait = forever`.
4747

4848
The fix: California's node sends **null messages** periodically. A null message says:
4949

@@ -78,7 +78,7 @@ reactor GridServer(null_message_period: time = 1 s) {
7878
}
7979
```
8080

81-
The timer fires every second. If no real command has arrived, a `0` is forwarded which the grid manager interprets as "no change." This lets the grid manager advance its logical time by at least 1 second per second, bounding the wait.
81+
The timer fires every second. If no real command has arrived, a `0` is forwarded, which the grid manager interprets as "no change." This lets the grid manager advance its logical time by at least 1 second per second, bounding the wait.
8282

8383
---
8484

@@ -89,7 +89,7 @@ The timer fires every second. If no real command has arrived, a `0` is forwarded
8989
- **Bounded wait**: the maximum wait is ~1 second (the null message period) plus network latency.
9090

9191
**What you give up:**
92-
- **Availability under failure**: if the California `GridServer` crashes, it stops sending null messages. New York's manager blocks indefinitely — it cannot advance time because it has no proof that California won't send a message.
92+
- **Availability under failure**: if the California `GridServer` crashes, it stops sending null messages. New York's manager blocks indefinitely because it has no proof that California won't send a message.
9393
- **Network overhead**: null messages are sent every second even when there are no real commands. For a grid with hundreds of nodes, this multiplies.
9494

9595
This is the CAL theorem in action: **strong consistency costs availability when network behavior is unbounded**.
@@ -98,9 +98,7 @@ This is the CAL theorem in action: **strong consistency costs availability when
9898

9999
## Code
100100

101-
See [`src/DistibutedPowerGrid4_ChandyMisra.lf`](src/DistibutedPowerGrid4_ChandyMisra.lf). And here is what our system looks like:
102-
103-
And here is what our system looks like:
101+
See [`src/Step4_Conservative.lf`](src/Step4_Conservative.lf). Here is what our system looks like:
104102

105103
![DistibutedPowerGrid4 Chandy & Misra](fig/DistibutedPowerGrid4_ChandyMisra.svg)
106104

@@ -121,4 +119,4 @@ Key changes from Step 3:
121119

122120
---
123121

124-
**Next:** [Step 5 Hybrid Design: Fast-Path for Safe Commands](05-hybrid.md)
122+
**Next:** [Step 5: Hybrid Design: Fast-Path for Safe Commands](05-hybrid.md)

0 commit comments

Comments
 (0)