Skip to content
Merged
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
24 changes: 15 additions & 9 deletions .claude/skills/fizz-spec/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ role Server:
return self.data.get(key, None)

action Write:
key = any self.pending_keys
key = oneof self.pending_keys
self.data[key] = self.pending[key]
```

Expand Down Expand Up @@ -133,10 +133,16 @@ action Choose:
### Nondeterminism

```python
x = any [1, 2, 3] # pick any element
x = any range(10) # pick any integer 0..9
x = any items # pick any element from collection
# Note: any on empty collection disables the action
x = oneof [1, 2, 3] # pick one element (nondeterministic)
x = oneof range(10) # pick one integer 0..9
x = oneof items # pick one element from collection
# Note: oneof on empty collection disables the action

oneof x in items: # for-each style: pick one x, execute body
process(x)

# `any` is a deprecated alias for `oneof` (emits DeprecationWarning)
x = any items # old form — still works but deprecated
```

### Assertions
Expand Down Expand Up @@ -171,7 +177,7 @@ id = IDS.fresh()

# Interchangeable pool (e.g., task content)
TEXTS = symmetry.nominal(name="task", limit=3)
v = any TEXTS.choices() # grows naturally: 1 option first, then more
v = oneof TEXTS.choices() # grows naturally: 1 option first, then more

# Symmetric roles (N! reduction for N instances)
symmetric role Worker:
Expand Down Expand Up @@ -229,8 +235,8 @@ Cross-role function calls can also be lost (message loss simulation) — model c
3. **Role Init ordering**: create globals before creating roles that depend on them
4. **`require` ≠ assertion**: `require x >= 0` disables action; use `always assertion` for invariants
5. **Lists break symmetry**: use `bag()` not `[]` to hold symmetric role instances
6. **`any` on empty**: disables the action (no `require len > 0` needed)
7. **`any` keyword vs Python `any()` function**: `x = any([cond for ...])` is a parse error — `any` is the nondeterministic choice keyword. For assignments use `all([...])` or `len([x for x in xs if cond]) > 0`. In guards, `require all([not cond for ...])` is the idiomatic form.
6. **`oneof`/`any` on empty**: disables the action (no `require len > 0` needed)
7. **`any` keyword vs Python `any()` function**: `x = any([cond for ...])` is a parse error — `any` is the deprecated nondeterministic choice keyword. Prefer `oneof` (no collision: `oneof` can't be an identifier). For boolean checks use `all([...])` or `len([x for x in xs if cond]) > 0`.
8. **Function calls**: can only call fizz functions from atomic context or inside roles; extract to variable before using in expressions
9. **`None` works**: use `== None`, not `is None` (no `is` operator)

Expand Down Expand Up @@ -294,7 +300,7 @@ action Hire:
employees.add(Employee())

action Fire:
target = any [e for e in employees if e.active]
target = oneof [e for e in employees if e.active]
employees = bag([e for e in employees if e.__id__ != target.__id__])
```

Expand Down
2 changes: 1 addition & 1 deletion examples/references/05-01-any-statement/Choice.fizz
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ atomic action ChooseOne:

# Any picks one element nondeterministically
# Model checker explores all possibilities
chosen = any options
chosen = oneof options

# Creates branches for each option: chosen=1, chosen=2, chosen=3

Expand Down
4 changes: 2 additions & 2 deletions examples/references/05-02-any-with-condition/Choice.fizz
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ atomic action ChoosePair:
require first == 0 or second == 0

# Choose two different values from options
first = any options
second = any options if second != first
first = oneof options
second = oneof options if second != first

# Safety: first and second must be different (unless both 0)
always assertion DifferentValues:
Expand Down
2 changes: 1 addition & 1 deletion examples/references/05-03-any-fairness/Choice.fizz
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ action Init:

atomic fair<strong> action SelectValue:
# Fair any ensures all indices eventually chosen
idx = fair any range(len(values))
idx = fair oneof range(len(values))
values[idx] = True

# Liveness: all values eventually become True
Expand Down
8 changes: 4 additions & 4 deletions examples/references/12-03-advanced-any-patterns/Patterns.fizz
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ action Init:
atomic action ChooseDifferentPair:
require first == 0 or second == 0

first = any options
first = oneof options
# Choose different value using condition
second = any options if second != first
second = oneof options if second != first

# Combine fair any with range
atomic fair<strong> action FairChoice:
require fair_choice == 0

# Ensures all values eventually chosen
fair_choice = fair any options
fair_choice = fair oneof options

# Can also combine fair any with filtering by using separate variables
atomic fair<strong> action FairFilteredChoice:
require len(options) > 0

# Fair choice from available options
idx = fair any range(len(options))
idx = fair oneof range(len(options))
chosen = options[idx]

# Safety: first and second are different
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ role Participant:

func placehold():
# Participant votes on transaction
vote = any ["accepted", "aborted"]
vote = oneof ["accepted", "aborted"]
self.status = vote
return self.status

Expand Down
4 changes: 2 additions & 2 deletions examples/references/13-03-01-consensus/Consensus.fizz
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ atomic action Propose:
require proposal_value == None

# Nondeterministically choose value to propose
proposal_value = any [1, 2]
proposal_value = oneof [1, 2]

atomic action Accept:
require proposal_value != None
require decided_value == None

# Node accepts proposal
node = any ["node_0", "node_1", "node_2"]
node = oneof ["node_0", "node_1", "node_2"]
require node not in acceptances

acceptances.add(node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ atomic action RetryRequest:
require requests > 0

# Retry with existing request ID
request_id = any range(requests)
request_id = oneof range(requests)

# Idempotent: no effect if already processed
if request_id not in processed_ids:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ atomic action TurnOn:
# Model checker explores all possible choices
# With symmetric values: states where k0=ON,k1=OFF,k2=OFF
# are equivalent to k1=ON,k0=OFF,k2=OFF
key = any KEYS
key = oneof KEYS
if switches[key] == 'OFF':
switches[key] = 'ON'

atomic action TurnOff:
key = any KEYS
key = oneof KEYS
if switches[key] == 'ON':
switches[key] = 'OFF'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ action Init:
atomic action Put:
# choices() returns existing IDs + one fresh ID (if under limit).
# Use with `any` for nondeterministic selection.
id = any IDS.choices()
id = oneof IDS.choices()
cache[id] = "data"

atomic action Evict:
# `any` on an empty list disables the transition, so no guard needed.
id = any IDS.values()
# `oneof` on an empty list disables the transition, so no guard needed.
id = oneof IDS.values()
if id in cache:
cache.pop(id)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ action Init:
atomic action InsertAnywhere:
# Get all gaps between existing timestamps
gaps = TIMES.segments()
gap = any gaps
gap = oneof gaps
t = gap.fresh()

atomic action InsertBetween:
# Insert strictly between t_start and t_end.
# Ordinal gaps are always non-empty (the domain is dense).
gaps = TIMES.segments(after=t_start, before=t_end)
gap = any gaps
gap = oneof gaps
t = gap.fresh()
# Guaranteed: t_start < t < t_end

Expand All @@ -37,7 +37,7 @@ atomic action InsertBeforeEarliest:
# (Inserting after max is just fresh(), but before min needs segments.)
earliest = TIMES.min()
gaps = TIMES.segments(before=earliest)
gap = any gaps
gap = oneof gaps
t = gap.fresh()

# Note: domain methods like values() cannot be called from assertions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ atomic action Place:

atomic action Advance:
require len(positions) > 0
p = any positions
p = oneof positions
next_p = p + 1 # wraps mod 5
if next_p not in positions:
positions.remove(p)
Expand Down
6 changes: 3 additions & 3 deletions examples/references/16-12-materialize/Materialize.fizz
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ action Init:
status[n] = "idle"

atomic action Activate:
n = any NODES.values()
n = oneof NODES.values()
require status[n] == "idle"
status[n] = "active"

atomic action Deactivate:
n = any NODES.values()
n = oneof NODES.values()
require status[n] == "active"
status[n] = "idle"

atomic action PassToken:
n = any NODES.values()
n = oneof NODES.values()
require status[n] == "active"
next_n = n + 1 # wraps mod 4
status[n] = "idle"
Expand Down
2 changes: 1 addition & 1 deletion examples/references/99-01-checkpoints/Checkpoints.fizz
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ role Coordinator:
# Checkpoints work in atomic actions too
# They don't create yield points, just visualization markers
`evaluating`
choice = any ["approve", "reject"]
choice = oneof ["approve", "reject"]

`decision_made`
self.decision = choice
Expand Down
20 changes: 15 additions & 5 deletions examples/references/GOTCHAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,11 @@ reusable because new instances are interchangeable with old ones.
**Symptom**: Parse error or unexpected behavior when using `any(...)` to
check if at least one element in a collection satisfies a condition.

**Cause**: `any` is a FizzBee keyword for nondeterministic choice. The
parser tries to interpret `any` as the nondeterministic choice operator
first. When used in an assignment like `x = any([...])`, the parser sees
it as a nondeterministic choice over a list literal, not a function call,
causing a parse error or wrong semantics.
**Cause**: `any` is a FizzBee keyword for nondeterministic choice
(deprecated — prefer `oneof`). The parser tries to interpret `any` as the
nondeterministic choice operator first. When used in an assignment like
`x = any([...])`, the parser sees it as a nondeterministic choice over a
list literal, not a function call, causing a parse error or wrong semantics.

In some contexts (e.g., directly inside `if` or `require`) the parser
falls back to treating `any` as an identifier, so the Python `any()`
Expand Down Expand Up @@ -255,6 +255,16 @@ if not any([x > 0 for x in items]):
`len([...]) > 0` for assignments. For guards, `require all([not ...])` is
the idiomatic FizzBee style anyway (see Performance Guide).

**Note**: The `oneof` keyword (preferred over `any` for nondeterministic
choice) does not have this collision — `oneof` cannot be used as an
identifier, so `oneof([...])` is always a parse error. Use `oneof` to
avoid the ambiguity entirely:

```python
x = oneof items # preferred (no collision risk)
x = any items # deprecated (emits DeprecationWarning)
```

---

## 12. Function Calls Only from Atomic Context or Roles
Expand Down
Loading