Skip to content

Commit 06cf651

Browse files
committed
Update Ghosts Section
1 parent 7652fd9 commit 06cf651

7 files changed

Lines changed: 139 additions & 97 deletions

File tree

getting-started/overview.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ LiquidJava is an additional type checker for Java. It extends Java with three ma
1111

1212
- **Liquid types** to restrict the values variables, parameters, and return values can have using logical predicates.
1313
- **Typestates** to describe valid object protocols across method calls.
14-
- **Ghost variables** to track extra state when typestates aren't enough.
14+
- **Ghosts** to track extra state when typestates aren't enough.
1515

1616
These make it possible to catch bugs that the standard Java type system cannot, including:
1717

@@ -23,8 +23,6 @@ These make it possible to catch bugs that the standard Java type system cannot,
2323
## Example
2424

2525
```java
26-
import liquidjava.specification.*;
27-
2826
public class Example {
2927
public static int divide(int a, @Refinement("b != 0") int b) {
3028
return a / b;

reference/external-refinements.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,42 @@
11
---
22
title: External Refinements
33
parent: Reference
4-
nav_order: 5
4+
nav_order: 4
55
---
66

77
# External Refinements
88

9-
External refinements let you attach a LiquidJava model to an existing class that you do not own.
9+
External refinements let us add refinements to an existing class that we cannot modify.
1010

11-
Use `@ExternalRefinementsFor` to describe the behavior of a library type in a separate interface. This lets you keep using the original library API in ordinary Java code while LiquidJava checks the extra specification in the background.
11+
For this, we use the `@ExternalRefinementsFor` annotation to specify the qualified name of the class we want to refine in a separate interface. This lets us refine external classes from the Java standard library, third-party dependencies, or shared APIs without modifying their source code.
1212

1313
```java
1414
import liquidjava.specification.*;
15+
import java.net.*;
1516

1617
@ExternalRefinementsFor("java.net.Socket")
17-
@StateSet({"unconnected, bound, connected, closed"})
18+
@StateSet({"unconnected", "bound", "connected", "closed"})
1819
public interface SocketRefinements {
19-
@StateRefinement(to = "unconnected(this)")
20+
@StateRefinement(to="unconnected()")
2021
public void Socket();
21-
22-
@StateRefinement(from = "unconnected(this)", to = "bound(this)")
23-
public void bind(SocketAddress bindpoint);
24-
25-
@StateRefinement(from = "bound(this)", to = "connected(this)")
26-
public void connect(SocketAddress endpoint);
27-
28-
@StateRefinement(from = "!closed(this)", to = "closed(this)")
22+
23+
@StateRefinement(from="unconnected()", to="bound()")
24+
public void bind(SocketAddress add);
25+
26+
@StateRefinement(from="bound()", to="connected()")
27+
public void connect(SocketAddress add);
28+
29+
@StateRefinement(from="connected()")
30+
public void sendUrgentData(int n);
31+
32+
@StateRefinement(from="!closed()", to="closed()")
2933
public void close();
3034
}
3135
```
3236

33-
This pattern is useful when you want to verify protocols for standard-library classes, third-party dependencies, or shared APIs without modifying their source code.
37+
```java
38+
Socket socket = new Socket();
39+
socket.connect(new InetSocketAddress("example.com", 80)); // type error!
40+
socket.close();
41+
```
3442

35-
The [Examples]({{ '/examples/' | relative_url }}) page links to runnable repositories that include this style of modeling.

reference/ghost-variables.md

Lines changed: 0 additions & 50 deletions
This file was deleted.

reference/ghost.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
title: Ghosts
3+
parent: Reference
4+
nav_order: 5
5+
---
6+
7+
# Ghosts
8+
9+
Some protocols need more than a small set of named states. LiquidJava supports ghost variables for tracking extra state. They can be used in refinements and state refinements to express richer invariants about the object. Similarly to the states, these are functions that take the object being refined as a parameter.
10+
11+
Ghosts are declared with the `@Ghost` annotation, and they can be updated with `@StateRefinement` annotations on methods.
12+
13+
```java
14+
import liquidjava.specification.*;
15+
16+
@ExternalRefinementsFor("java.util.Stack")
17+
@Ghost("int size")
18+
public interface StackRefinements<E> {
19+
20+
@StateRefinement(to="size() == 0")
21+
public void Stack();
22+
23+
@StateRefinement(to="size() == size(old(this)) + 1")
24+
public E push(E elem);
25+
26+
@StateRefinement(from="size() > 0", to="size() == size(old(this)) - 1")
27+
public E pop();
28+
29+
@StateRefinement(from="size() > 0")
30+
public E peek();
31+
}
32+
````
33+
34+
```java
35+
Stack<String> s = new Stack<>();
36+
s.push("hello");
37+
s.pop();
38+
s.pop(); // type error!
39+
```
40+
41+
In the "constructor" method, the ghost variable `size` is initialized to 0. An equality in a method postcondition is how ghost variables are updated. However, here it is not necessary, since when no postcondition is declared, it is initialized to its default value, similarly to how Java initializes fields to their default values when no explicit initializer is provided (`int --> 0`, `boolean --> false`, etc.).
42+
43+
In the `push` method, we specify no precondition, since we can always push an element to the stack, but we specify a postcondition that increments the `size` by one. In this case, we tell the typechecker that the new value of `size` after calling `push` is equal to the old value of `size` plus one. This is possible using the `old` function, which takes an object instance and returns its value before the method call.
44+
45+
In the `pop` method, we specify a precondition that the `size` must be greater than zero, since we cannot pop from an empty stack. We also specify a postcondition that decrements the `size` by one, similarly to the `push` method.
46+
47+
In the `peek` method, we specify the same precondition, since we also cannot peek from an empty stack, but we don't specify a postcondition since peeking does not change the size of the stack.
48+

reference/index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ nav_order: 2
44
has_children: true
55
has_toc: false
66
permalink: /reference/
7-
description: Browse the LiquidJava reference for refinements, aliases, state refinements, ghost variables, and external refinements.
7+
description: Browse the LiquidJava reference for refinements, aliases, state refinements, ghosts, and external refinements.
88
cards:
99
- title: Refinements
1010
url: /reference/refinements/
@@ -15,8 +15,8 @@ cards:
1515
- title: State Refinements
1616
url: /reference/state-refinements/
1717
description: Learn how to model protocol-oriented object states and valid method transitions.
18-
- title: Ghost Variables
19-
url: /reference/ghost-variables/
18+
- title: Ghosts
19+
url: /reference/ghosts/
2020
description: Learn how to track logical state that helps express and verify richer object invariants.
2121
- title: External Refinements
2222
url: /reference/external-refinements/

reference/refinements.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import liquidjava.specification.*;
1515

1616
public class RefinementExamples {
1717
@Refinement("x > 0") // x must be greater than 0
18-
int x;
18+
int x = 1;
1919

2020
@Refinement("0 <= _ && _ <= 100") // y must be between 0 and 100
21-
int y;
21+
int y = 50;
2222

2323
@Refinement("z % 2 == 0 ? z >= 0 : z < 0") // z must be positive if even, negative if odd
24-
int z;
24+
int z = 4;
2525

2626
@Refinement("_ >= 0") // the return value must be non-negative
2727
int absDiv(int a, @Refinement("b != 0") int b) { // b must be non-zero
@@ -33,7 +33,7 @@ public class RefinementExamples {
3333

3434
## Predicate Syntax
3535

36-
The predicates allowed inside a refinement belong to quantifier-free linear integer arithmetic. In practice, this means you can write boolean expressions over integer values using comparisons, logical connectives, arithmetic operators, and conditional expressions. You can also call ghost variables and aliases from refinements, which we cover in later sections.
36+
The predicates allowed inside a refinement belong to quantifier-free linear integer arithmetic. In practice, this means you can write boolean expressions over integer values using comparisons, logical connectives, arithmetic operators, and conditional expressions. You can also call ghosts and aliases from refinements, which we cover in later sections.
3737

3838
| Form | Syntax | Example |
3939
| --- | --- | --- |

reference/state-refinements.md

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,84 @@ nav_order: 3
88

99
Beyond basic refinements, LiquidJava supports object state modeling through typestates. This lets you describe when a method can or cannot be called based on the state of the object.
1010

11-
## Example
11+
The possible states of a class are declared with `@StateSet`, and the state transitions are described with `@StateRefinement(from = "...", to = "...")`:
12+
- `from` describes the **precondition** - the object state in which the method can be invoked
13+
- `to` describes the **postcondition** - the state the object will have after the method is called
1214

1315
```java
14-
import liquidjava.specification.StateRefinement;
15-
import liquidjava.specification.StateSet;
16+
import liquidjava.specification.*;
1617

1718
@StateSet({"open", "closed"})
18-
public class MyFile {
19-
@StateRefinement(to = "open(this)")
20-
public MyFile() {}
19+
public class File {
20+
@StateRefinement(to = "open()")
21+
public File() {}
2122

22-
@StateRefinement(from = "open(this)", msg = "file must be open to read")
23+
@StateRefinement(from = "open()")
2324
public void read() {}
2425

25-
@StateRefinement(from = "open(this)", to = "closed(this)", msg = "file must be open to close")
26+
@StateRefinement(from = "open()", to = "closed()")
2627
public void close() {}
2728
}
29+
```
2830

29-
MyFile f = new MyFile();
31+
```java
32+
File f = new File();
3033
f.read();
3134
f.close();
32-
f.read(); // type error: file must be open to read
35+
f.read(); // type error!
36+
```
37+
38+
If a class follows an implicit protocol that can be described by a DFA, the protocol can be encoded in LiquidJava so that methods are enforced to be called in the correct order.
39+
40+
## Syntax
41+
42+
Note that states are functions. These take a single parameter, which is the object being refined. Since we are describing the state of the current object, we use `this` as the parameter, which is being used implicitly in the example above. Actually, all of these are equivalent: `open()`, `this.open()`, and `open(this)`.
43+
44+
## State Initialization
45+
46+
Constructors can only declare a `to` transition, since they are responsible for establishing the initial state of the object.
47+
48+
```java
49+
import liquidjava.specification.*;
50+
51+
@StateSet({"new", "ready"})
52+
public class Buffer {
53+
@StateRefinement(to="ready()")
54+
public Buffer() {}
55+
}
3356
```
3457

35-
This encodes a simple protocol:
58+
If no `to` transition is written, LiquidJava defaults the constructor to the first state listed in the corresponding `@StateSet`.
3659

37-
- construction produces an open file
38-
- `read` requires the file to be open
39-
- `close` requires the file to be open and transitions it to closed
60+
Constructors must always be present for typestate checking to work correctly, because they are the point where the initial state values are assigned. Otherwise, the initial values are not set and the verifier won't be able to track the state of the object across method calls, which can lead to unexpected type errors.
4061

41-
An illegal call sequence, such as reading after closing, becomes a verification error. As with refinements, you can provide custom error messages to make violations easier to diagnose.
62+
When refining interfaces, there is no real constructor, but LiquidJava still needs an initialization point. In those cases, if the type is named `Interface`, it must declare a method with the signature `public void Interface()` so the initial values are set correctly. This method plays the role of a constructor for the typestate system.
4263

43-
## Why This Matters
4464

45-
Typestates are especially helpful for:
65+
## Multiple StateSets
66+
67+
Classes can declare more than one `@StateSet` annotation. This is useful when the object has independent, orthogonal dimensions of state.
68+
69+
```java
70+
import liquidjava.specification.*;
71+
72+
@StateSet({"open", "closed"})
73+
@StateSet({"clean", "dirty"})
74+
public class Device {
75+
@StateRefinement(to="open() && clean()")
76+
public Device() {}
77+
78+
@StateRefinement(from="open() && clean()", to="open() && dirty()")
79+
public void use() {}
80+
81+
@StateRefinement(from="open() && dirty()", to="open() && clean()")
82+
public void clean() {}
83+
84+
@StateRefinement(from="open() && clean()", to="closed() && clean()")
85+
public void close() {}
86+
}
87+
```
4688

47-
- files and sockets
48-
- locks and synchronization primitives
49-
- lifecycle-driven components
50-
- APIs with strict call ordering rules
89+
Each state set is exclusive: at any moment, the object can only be in one state from that specific set. In the example above, the object cannot be both `open` and `closed`, and it cannot be both `clean` and `dirty`.
5190

52-
Continue with [Ghost Variables]({{ '/reference/ghost-variables/' | relative_url }}) and [External Refinements]({{ '/reference/external-refinements/' | relative_url }}) for protocols that need richer tracking.
91+
When a transition affects multiple orthogonal states, the states must be combined with `&&` in the `from` and `to` annotations. In the example above, the `use` method transitions from `open && clean` to `open && dirty`, and the `close` method transitions from `open && clean` to `closed && clean`.

0 commit comments

Comments
 (0)