Skip to content

Commit f216518

Browse files
authored
Merge pull request #474 from hedgehogqa/remove-env
Simplify vars and envs
2 parents 9635160 + 6d1aef0 commit f216518

18 files changed

Lines changed: 225 additions & 389 deletions

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,7 @@ tests/Hedgehog.Tests/tests-js/
7878
BenchmarkDotNet.Artifacts
7979

8080
# docfx
81-
_site/
81+
_site/
82+
83+
# tests / verifier
84+
*.received.*

src/Hedgehog.Stateful/Hedgehog.Stateful.fsproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
<ItemGroup>
3131
<Compile Include="Types.fs" />
3232
<Compile Include="Var.fs" />
33-
<Compile Include="StateFormatter.fs" />
3433
<Compile Include="Action.fs" />
3534
<Compile Include="ICommand.fs" />
3635
<Compile Include="Command.fs" />

src/Hedgehog.Stateful/Parallel.fs

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ module Parallel =
3232
let stateAfterSetup =
3333
setup.Steps
3434
|> List.fold (fun s a ->
35-
let name, _ = Env.freshName Env.empty
36-
a.Update s (Var.bound name)
35+
a.Update s (Var.symbolicEmpty())
3736
) initial
3837

3938
// Generate prefix using state after setup
@@ -43,8 +42,7 @@ module Parallel =
4342
let stateAfterPrefix =
4443
prefix.Steps
4544
|> List.fold (fun s a ->
46-
let name, _ = Env.freshName Env.empty
47-
a.Update s (Var.bound name)
45+
a.Update s (Var.symbolicEmpty())
4846
) stateAfterSetup
4947

5048
// Generate branches from state after prefix
@@ -85,11 +83,10 @@ module Parallel =
8583
match Map.tryFind action.Id results with
8684
| None -> None
8785
| Some output ->
88-
let name, env' = Env.freshName env
89-
let outputVar = Var.bound name
90-
let env'' = Env.add outputVar output env'
86+
let _, env' = Env.freshName env
87+
let outputVar = Concrete output
9188
let state' = action.Update state outputVar
92-
Some (state', env'')
89+
Some (state', env')
9390

9491
/// Recursively search for a valid interleaving of the two branches.
9592
/// Returns true as soon as a valid interleaving is found (early termination).
@@ -152,12 +149,12 @@ module Parallel =
152149
match result with
153150
| ActionResult.Failure ex ->
154151
do! Property.counterexample (fun () -> formatActionName action)
155-
do! Property.counterexample (fun () -> $"Final state: %A{StateFormatter.formatForDisplay env state}")
152+
do! Property.counterexample (fun () -> $"Final state: %A{state}")
156153
return! Property.exn ex
157154
| ActionResult.Success output ->
158-
let name, env' = Env.freshName env
159-
let outputVar = Var.bound name
160-
env <- Env.add outputVar output env'
155+
let _, env' = Env.freshName env
156+
let outputVar = Concrete output
157+
env <- env'
161158
state <- action.Update state outputVar
162159

163160
// Run prefix sequentially
@@ -169,13 +166,13 @@ module Parallel =
169166
match result with
170167
| ActionResult.Failure ex ->
171168
do! Property.counterexample (fun () -> formatActionName action)
172-
do! Property.counterexample (fun () -> $"Final state: %A{StateFormatter.formatForDisplay env state}")
169+
do! Property.counterexample (fun () -> $"Final state: %A{state}")
173170
return! Property.exn ex
174171
| ActionResult.Success output ->
175172
prefixResults.Add(action.Id, output)
176-
let name, env' = Env.freshName env
177-
let outputVar = Var.bound name
178-
env <- Env.add outputVar output env'
173+
let _, env' = Env.freshName env
174+
let outputVar = Concrete output
175+
env <- env'
179176
state <- action.Update state outputVar
180177

181178
// Save state and env before parallel branches (which is also before cleanup)
@@ -193,11 +190,10 @@ module Parallel =
193190
| ActionResult.Failure ex ->
194191
return Error ex
195192
| ActionResult.Success output ->
196-
let name, env' = Env.freshName branchEnv
197-
let outputVar = Var.bound name
198-
let newEnv = Env.add outputVar output env'
193+
let _, env' = Env.freshName branchEnv
194+
let outputVar = Concrete output
199195
let newState = action.Update branchState outputVar
200-
return! loop ((action.Id, output) :: results) newEnv newState rest
196+
return! loop ((action.Id, output) :: results) env' newState rest
201197
}
202198
loop [] env state branch
203199

@@ -215,7 +211,7 @@ module Parallel =
215211
| Error ex, _ | _, Error ex ->
216212
// Branch failed - report state before branches
217213
property {
218-
do! Property.counterexample (fun () -> $"Final state: %A{StateFormatter.formatForDisplay envBeforeBranches stateBeforeBranches}")
214+
do! Property.counterexample (fun () -> $"Final state: %A{stateBeforeBranches}")
219215
return! Property.exn ex
220216
}
221217
| Ok results1, Ok results2 ->
@@ -229,7 +225,7 @@ module Parallel =
229225
if not linearizable then
230226
property {
231227
do! Property.counterexample (fun () -> "No valid interleaving found")
232-
do! Property.counterexample (fun () -> $"Final state: %A{StateFormatter.formatForDisplay envBeforeBranches stateBeforeBranches}")
228+
do! Property.counterexample (fun () -> $"Final state: %A{stateBeforeBranches}")
233229
return! Property.failure
234230
}
235231
else
@@ -249,9 +245,10 @@ module Parallel =
249245
| ActionResult.Failure ex ->
250246
cleanupError <- Some (formatActionName action, ex)
251247
| ActionResult.Success output ->
252-
let name, env' = Env.freshName env
253-
let outputVar = Var.bound name
254-
env <- Env.add outputVar output env'
248+
let _, env' = Env.freshName env
249+
let outputVar = Concrete output
250+
env <- env'
251+
state <- action.Update state outputVar
255252

256253
// Check linearizability first
257254
do! linearizabilityCheck

src/Hedgehog.Stateful/Sequential.fs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ module Sequential =
4242

4343
// Use outcome to compute next state for subsequent actions
4444
let outcomeAction, outcomeEnv = Tree.outcome actionAndEnvTree
45-
let name, nextEnv = Env.freshName outcomeEnv
46-
let outputVar = Var.bound name
45+
let _, nextEnv = Env.freshName outcomeEnv
46+
let outputVar = Var.symbolicEmpty()
4747
let nextState = outcomeAction.Update currentState outputVar
4848

4949
// Recursively generate rest with updated state
@@ -94,8 +94,8 @@ module Sequential =
9494

9595
// Extract outcome for state evolution
9696
let action, env' = Tree.outcome actionAndEnvTreeResult
97-
let name = fst (Env.freshName env)
98-
let outputVar = Var.bound name
97+
let _, _ = Env.freshName env
98+
let outputVar = Var.symbolicEmpty()
9999
let state' = action.Update state outputVar
100100

101101
// Recurse with split seed
@@ -112,7 +112,7 @@ module Sequential =
112112
| [] -> true
113113
| action :: rest ->
114114
if action.Require env state then
115-
let state' = action.Update state (Var.bound (Name counter))
115+
let state' = action.Update state (Var.symbolicEmpty())
116116
validateSequence env state' (counter + 1) rest
117117
else false
118118

@@ -121,8 +121,8 @@ module Sequential =
121121
let allActions = setupActions @ testActions
122122
allActions
123123
|> List.indexed
124-
|> List.fold (fun state (counter, action) ->
125-
action.Update state (Var.bound (Name counter))
124+
|> List.fold (fun state (_counter, action) ->
125+
action.Update state (Var.symbolicEmpty())
126126
) initial
127127

128128
// Main generator
@@ -201,13 +201,12 @@ module Sequential =
201201
// Add counterexample for the failing action before propagating the exception
202202
do! Property.counterexample (fun () -> formatActionName action)
203203
if action.Category <> ActionCategory.Cleanup then
204-
do! Property.counterexample (fun () -> $"Failed at state: %A{StateFormatter.formatForDisplay env state}")
204+
do! Property.counterexample (fun () -> $"Failed at state: %A{state}")
205205
do! Property.exn ex
206206

207207
| ActionResult.Success output ->
208-
let name, env' = Env.freshName env
209-
let outputVar = Var.bound name
210-
let env'' = Env.add outputVar output env'
208+
let _, env' = Env.freshName env
209+
let outputVar = Concrete output
211210
let state0 = state
212211
let state1 = action.Update state outputVar
213212

@@ -216,19 +215,19 @@ module Sequential =
216215
// If ensure fails, add final state before propagating
217216
do!
218217
try
219-
action.Ensure env'' state0 state1 output |> Property.ofBool
218+
action.Ensure env' state0 state1 output |> Property.ofBool
220219
with ex ->
221220
if action.Category <> ActionCategory.Cleanup then
222221
property {
223-
do! Property.counterexample (fun () -> $"Failed at state: %A{StateFormatter.formatForDisplay env'' state1}")
222+
do! Property.counterexample (fun () -> $"Failed at state: %A{state1}")
224223
do! Property.exn ex
225224
}
226225
else
227226
Property.exn ex
228-
do! loop state1 env'' rest
227+
do! loop state1 env' rest
229228
}
230229

231230
property {
232-
do! Property.counterexample (fun () -> $"Initial state: %A{StateFormatter.formatForDisplay Env.empty actions.Initial}")
231+
do! Property.counterexample (fun () -> $"Initial state: %A{actions.Initial}")
233232
do! loop actions.Initial Env.empty actions.Steps
234233
}

src/Hedgehog.Stateful/Types.fs

Lines changed: 40 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -8,135 +8,80 @@ type Name = internal Name of int
88

99

1010
/// <summary>
11-
/// Environment mapping symbolic variable names to concrete values.
11+
/// Environment used for generating unique action IDs and as a capability token
12+
/// to enforce compile-time safety. The presence of an Env parameter indicates
13+
/// the execution phase, where symbolic variables can be resolved.
1214
/// The SUT is passed separately to Execute, not stored in Env.
1315
/// </summary>
1416
type Env = private {
15-
values: Map<Name, obj>
1617
nextId: int
1718
}
1819

1920

2021
/// <summary>
2122
/// Symbolic variable referencing a command's output. Symbolic variables are placeholders
2223
/// that let us chain commands by using one command's result as input to another, even before execution.
23-
/// A symbolic variable is not yet bound to a generated value and is used to represent a value in the model before binding occurs.
24+
/// Variables can be either Symbolic (during generation, with optional default values) or
25+
/// Concrete (during execution, with actual runtime values).
2426
/// </summary>
2527
[<StructuredFormatDisplay("{DisplayText}")>]
26-
type Var<'T> = private {
28+
type Var<'T> =
29+
internal
2730
/// <summary>
28-
/// The unique integer name of the variable.
31+
/// Symbolic variable with an optional default value.
32+
/// None indicates a placeholder created during generation (not yet executed).
33+
/// Some indicates an initial state default value.
2934
/// </summary>
30-
Name: int
35+
| Symbolic of 'T option
3136
/// <summary>
32-
/// Indicates if the variable is bound to a generated value.
37+
/// Concrete variable with an actual runtime value from execution.
3338
/// </summary>
34-
Bounded: bool
35-
/// <summary>
36-
/// The optional default value for the variable.
37-
/// </summary>
38-
Default: 'T option
39-
/// <summary>
40-
/// Transform function applied when resolving the variable from the environment.
41-
/// Handles unboxing and any projections/mappings applied via Var.map.
42-
/// </summary>
43-
Transform: obj -> 'T
44-
/// <summary>
45-
/// Mutable field used internally for displaying resolved values in counterexamples.
46-
/// Only mutated during test failure formatting, not during normal test execution.
47-
///
48-
/// The purpose is to report failure with the concrete state values,
49-
/// to make dev experience better when debugging failed tests.
50-
///
51-
/// NOT INTENDED TO BE USED FOR ANY OTHER PURPOSE.
52-
/// </summary>
53-
mutable ResolvedValue: 'T option
54-
}
39+
| Concrete of 'T
5540
with
5641
/// <summary>
57-
/// Gets the unique integer name of the variable.
42+
/// Gets whether the variable has been bound to a concrete execution value.
5843
/// </summary>
59-
member this.VarName = this.Name
60-
61-
/// <summary>
62-
/// Gets whether the variable is bound to a generated value.
63-
/// </summary>
64-
member this.IsBounded = this.Bounded
44+
member this.IsBounded =
45+
match this with
46+
| Concrete _ -> true
47+
| Symbolic _ -> false
6548

6649
member private this.DisplayText =
67-
// If resolved for display (during counterexample formatting), show the resolved value
68-
match this.ResolvedValue with
69-
| Some resolved -> $"%A{resolved}"
70-
| None ->
71-
if this.Bounded then
72-
match this.Default with
73-
| Some d -> $"%A{d}"
74-
| None -> $"Var_%d{this.Name}"
75-
else
76-
match this.Default with
77-
| Some d -> $"%A{d}"
78-
| None -> "<no value> (symbolic)"
50+
match this with
51+
| Concrete value -> $"%A{value}"
52+
| Symbolic (Some defaultValue) -> $"%A{defaultValue}"
53+
| Symbolic None -> "<symbolic>"
7954

8055
/// <summary>
81-
/// Resolve the variable using its default if not found in the environment.
56+
/// Resolve the variable to its value.
57+
/// Requires Env parameter as a capability token to enforce that resolution
58+
/// only happens during execution phase (Execute, Require, Ensure methods).
8259
/// </summary>
83-
/// <param name="env">The environment to resolve the variable from.</param>
60+
/// <param name="env">The environment capability token.</param>
8461
/// <returns>The resolved value of the variable.</returns>
8562
member this.Resolve(env: Env) : 'T =
86-
if not this.Bounded then
87-
match this.Default with
88-
| Some d -> d
89-
| None -> failwithf "Symbolic var must have a default value"
90-
else
91-
match Map.tryFind (Name this.Name) env.values with
92-
| Some v -> this.Transform v
93-
| None ->
94-
match this.Default with
95-
| Some d -> d
96-
| None -> failwithf $"Var %A{Name this.Name} not bound in environment and no default provided"
63+
match this with
64+
| Concrete value -> value
65+
| Symbolic (Some defaultValue) -> defaultValue
66+
| Symbolic None -> failwith "Cannot resolve symbolic variable without a default value"
9767

9868
/// <summary>
99-
/// Resolve the variable with an explicit fallback value, overriding the variable's default.
69+
/// Resolve the variable with an explicit fallback value.
10070
/// </summary>
101-
/// <param name="env">The environment to resolve the variable from.</param>
102-
/// <param name="fallback">The fallback value to use if the variable is not found.</param>
103-
/// <returns>The resolved value or the fallback if not found.</returns>
71+
/// <param name="env">The environment capability token.</param>
72+
/// <param name="fallback">The fallback value to use if the variable is symbolic without a default.</param>
73+
/// <returns>The resolved value or the fallback.</returns>
10474
member this.ResolveOr(env: Env, fallback: 'T) : 'T =
105-
if not this.Bounded then
106-
fallback // Override default for unbounded
107-
else
108-
match Map.tryFind (Name this.Name) env.values with
109-
| Some v -> this.Transform v
110-
| None -> fallback
111-
112-
/// <summary>
113-
/// Set the resolved value for display purposes during counterexample formatting.
114-
/// This should only be called internally by StateFormatter during test failure formatting.
115-
/// </summary>
116-
/// <param name="env">The environment to resolve the variable from.</param>
117-
member internal this.SetResolvedValue(env: Env) : unit =
118-
try
119-
let resolved = this.Resolve(env)
120-
this.ResolvedValue <- Some resolved
121-
with
122-
| _ -> () // If resolution fails, leave ResolvedForDisplay as None
123-
124-
static member internal CreateSymbolic(value: 'T) : Var<'T> =
125-
{ Name = -1; Bounded = false; Default = Some value; Transform = unbox<'T>; ResolvedValue = Some value }
75+
match this with
76+
| Concrete value -> value
77+
| Symbolic (Some defaultValue) -> defaultValue
78+
| Symbolic None -> fallback
12679

12780

12881
module internal Env =
12982
/// Empty environment
130-
let empty : Env = { values = Map.empty; nextId = 0 }
83+
let empty : Env = { nextId = 0 }
13184

132-
/// Generate a fresh variable name
85+
/// Generate a fresh action ID
13386
let freshName (env: Env) : Name * Env =
13487
Name env.nextId, { env with nextId = env.nextId + 1 }
135-
136-
/// Store a concrete value for a variable
137-
let add (v: Var<'a>) (value: 'a) (env: Env) : Env =
138-
{ env with values = Map.add (Name v.Name) (box value) env.values }
139-
140-
/// Resolve a variable to its concrete value
141-
let resolve<'T> (v: Var<'T>) (env: Env) : 'T =
142-
v.Resolve(env)

0 commit comments

Comments
 (0)