Skip to content

Commit f0eb39e

Browse files
authored
Use original parameter names in symbolic evaluation output (#983)
## Use original parameter names in symbolic evaluation output ### Problem Generated variable names in verification condition output used opaque `$__` prefixed names (e.g., `$__top5`, `$__i3`, `$__S4`), making VCs hard to read. The name disambiguation logic was duplicated between the SMT encoder and the symbolic evaluator. ### Solution - Extract shared `@N` suffix disambiguation utilities into `Strata.Name` (`Strata/Util/Name.lean`) - Change `genSym` to prefer bare names and add `@N` suffixes only when the name is already in use, tracked via a `usedNames` set in `EvalConfig` - When a name already contains an `@N` suffix (e.g. a Strata parameter named `g@1`), `genSym` decomposes it and increments the suffix instead of appending a second `@` — producing `g@2` rather than `g@1@1` - Remove the now-dead `gen` and `varPrefix` fields from `EvalConfig` - `findUnique` uses `Std.HashSet String` for O(1) membership checks, with a fast counter-based search (`findUniqueFast`, 1M attempts) and a provably-terminating list-erasure fallback (`findUniqueSlow`) - Formal correctness proofs in `NameProofs.lean`: - `findUniqueFast_not_mem`: fast path result is not in `usedNames` - `findUniqueSlow_not_mem`: slow path result (when `some`) is not in `usedNames`, proved by strong induction on `remaining.length` - `findUniqueSlow_ne_none`: slow path never returns `none` when `remaining` covers `usedSet`, given `DisambiguateInjective` (a named proposition for suffix injectivity) - `findUnique_not_mem`: combined correctness for `findUnique`, fully proved assuming `DisambiguateInjective` (no `sorry`) - Add `@` to the DDM parser's `strataIsIdRest` so that SMT model responses with `@N`-suffixed variable names are parsed correctly - Sanitize SMT-LIB names starting with `@` or `.` (reserved in SMT-LIB) by prefixing with `$` - Pipe-quote variable names in SMT `get-value` commands so names containing `@` are valid ### Testing All existing tests pass with updated expectations. No simplification regressions — all obligations that simplified to `true` on main still simplify to `true`. Added a dedicated test (`AtSignDisambiguationTest`) verifying that a parameter named `g@1` (containing `@`) does not collide with the `@N` disambiguation suffix of another parameter `g`.
1 parent e1d6beb commit f0eb39e

55 files changed

Lines changed: 840 additions & 633 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Strata.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Strata.DL.Lambda.Lambda
1717
import Strata.DL.Imperative.Imperative
1818

1919
/- Utilities -/
20+
import Strata.Util.NameProofs
2021
import Strata.Util.Sarif
2122

2223
/- Strata Languages -/

Strata/DDM/Parser.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ private def strataIsIdFirst (c : Char) : Bool :=
123123
c.isAlpha || c == '_' || c == '$'
124124

125125
private def strataIsIdRest (c : Char) : Bool :=
126-
c.isAlphanum || c == '_' || c == '\'' || c == '.' || c == '?' || c == '!' || c == '$'
126+
c.isAlphanum || c == '_' || c == '\'' || c == '.' || c == '?' || c == '!' || c == '$' || c == '@'
127127

128128
private def isIdFirstOrBeginEscape (c : Char) : Bool :=
129129
strataIsIdFirst c || isIdBeginEscape c

Strata/DL/Lambda/LState.lean

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module
77

88
public import Strata.DL.Lambda.Factory
99
public import Strata.DL.Lambda.Scopes
10+
public import Strata.Util.Name
1011

1112
/-! ## State for (Partial) Evaluation of Lambda Expressions
1213
@@ -23,40 +24,30 @@ variable {T : LExprParams} [Inhabited T.Metadata] [BEq T.Metadata] [DecidableEq
2324
---------------------------------------------------------------------
2425

2526
/-
26-
Configuration for symbolic execution, where we have `gen` for keeping track of
27-
fresh `fvar`'s numbering. We also have a `fuel` argument for the evaluation
28-
function, and support for factory functions.
29-
30-
We rely on the parser disallowing Lambda variables to begin with `$__`, which is
31-
reserved for internal use. Also see `TEnv.genExprVar` used during type inference
32-
and `LState.genVar` used during evaluation.
27+
Configuration for symbolic execution, where we have `usedNames` for tracking
28+
which variable names have been generated. We also have a `fuel` argument for
29+
the evaluation function, and support for factory functions.
3330
-/
3431
structure EvalConfig (T : LExprParams) where
3532
factory : @Factory T
3633
fuel : Nat := 200
37-
varPrefix : String := "$__"
38-
gen : Nat := 0
34+
usedNames : Std.HashSet String := {}
3935

4036
instance : ToFormat (EvalConfig T) where
4137
format c :=
4238
f!"Eval Depth: {(repr c.fuel)}" ++ Format.line ++
43-
f!"Variable Prefix: {c.varPrefix}" ++ Format.line ++
44-
f!"Variable gen count: {c.gen}" ++ Format.line ++
4539
f!"Factory Functions:" ++ Format.line ++
4640
Std.Format.joinSep c.factory.toArray.toList f!"{Format.line}"
4741

4842
def EvalConfig.init : EvalConfig T :=
4943
{ factory := @Factory.default T,
5044
fuel := 200,
51-
gen := 0 }
52-
53-
def EvalConfig.incGen (c : EvalConfig T) : EvalConfig T :=
54-
{ c with gen := c.gen + 1 }
45+
usedNames := {} }
5546

5647
def EvalConfig.genSym (x : String) (c : EvalConfig T) : String × EvalConfig T :=
57-
let new_idx := c.gen
58-
let c := c.incGen
59-
let new_var := c.varPrefix ++ x ++ toString new_idx
48+
let (base, startSuffix) := Strata.Name.breakDisambiguated x
49+
let new_var := Strata.Name.findUnique base startSuffix c.usedNames
50+
let c := { c with usedNames := c.usedNames.insert new_var }
6051
(new_var, c)
6152

6253
---------------------------------------------------------------------
@@ -122,7 +113,8 @@ def LState.knownVars (σ : LState T) : List T.Identifier :=
122113

123114
/--
124115
Generate a fresh (internal) identifier with the base name
125-
`x`; i.e., `σ.config.varPrefix ++ x`.
116+
`x`, reusing the bare name when possible and adding `@N` suffixes
117+
only when disambiguation is needed.
126118
-/
127119
def LState.genVar {IDMeta} [Inhabited IDMeta] [DecidableEq IDMeta] (x : String) (σ : LState ⟨Unit, IDMeta⟩) : String × LState ⟨Unit, IDMeta⟩ :=
128120
let (new_var, config) := σ.config.genSym x
@@ -151,7 +143,6 @@ def LState.genVars (xs : List String) (σ : (LState ⟨Unit, Unit⟩)) : (List S
151143
instance : ToFormat (T.Identifier × LState T) where
152144
format im :=
153145
f!"New Variable: {im.fst}{Format.line}\
154-
Gen in EvalConfig: {im.snd.config.gen}{Format.line}\
155146
{im.snd}"
156147

157148
---------------------------------------------------------------------

Strata/DL/SMT/Encoder.lean

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module
88
public import Strata.DL.SMT.DDMTransform.Translate
99
public import Strata.DL.SMT.Factory
1010
public import Strata.DL.SMT.Op
11+
public import Strata.Util.Name
1112
public import Strata.DL.SMT.Solver
1213
public import Strata.DL.SMT.Term
1314
public import Strata.DL.SMT.TermType
@@ -102,6 +103,8 @@ def smtReservedKeywords : List String :=
102103
"abs", "and", "distinct", "/", "=", ">", ">=", "ite", "=>",
103104
"div", "is_int", "<", "<=", "-", "mod", "*", "not", "or", "+",
104105
"to_int", "to_real", "xor",
106+
-- Nonlinear arithmetic theory symbols
107+
"exp", "sin", "cos", "tan", "sqrt", "pi",
105108
-- String theory symbols
106109
"str.at", "str.++", "str.contains", "str.from_code", "str.from_int",
107110
"str.in_re", "str.indexof", "str.is_digit", "str.<=", "str.len",
@@ -114,44 +117,18 @@ def smtReservedKeywords : List String :=
114117
-- Array theory symbols
115118
"select", "store"]
116119

117-
/-- Generate a disambiguated name by appending @suffix -/
118-
def disambiguateName (baseName : String) (suffix : Nat) : String :=
119-
s!"{baseName}@{suffix}"
120-
121-
/-- Parse a list of digit characters as a natural number. -/
122-
def digitsToNat (cs : List Char) : Nat :=
123-
cs.foldl (fun n c => n * 10 + (c.toNat - '0'.toNat)) 0
124-
125-
/-- Break a potentially disambiguated name into its base name and next suffix.
126-
If the name has an `@N` suffix, returns `(base, N + 1)`.
127-
Otherwise returns `(name, 1)`. -/
128-
def breakDisambiguatedName (name : String) : String × Nat :=
129-
let cs := name.toList
130-
let digitSuffix := cs.reverse.takeWhile Char.isDigit |>.reverse
131-
let rest := cs.reverse.dropWhile Char.isDigit |>.reverse
132-
match rest.reverse, digitSuffix with
133-
| '@' :: _, _ :: _ => (String.ofList rest.dropLast, digitsToNat digitSuffix + 1)
134-
| _, _ => (name, 1)
135-
136-
/-- Find a unique name by trying candidates with increasing suffixes.
137-
The `isUsed` predicate checks if a candidate name is already taken. -/
138-
def findUniqueName (baseName : String) (startSuffix : Nat) (isUsed : String → Bool) (limit : Nat := 1000) : String :=
139-
let rec loop (candidate : String) (suffix : Nat) (remaining : Nat) : String :=
140-
if h : remaining == 0 then candidate -- Fallback after limit attempts
141-
else if isUsed candidate then
142-
loop (disambiguateName baseName suffix) (suffix + 1) (remaining - 1)
143-
else
144-
candidate
145-
termination_by remaining
146-
decreasing_by
147-
have : remaining ≠ 0 := by intro h'; simp [h'] at h
148-
omega
149-
loop (if startSuffix == 1 then baseName else disambiguateName baseName (startSuffix - 1)) startSuffix limit
120+
/-- Sanitize a name for use in SMT-LIB. Symbols starting with `@` or `.` are
121+
reserved in SMT-LIB and rejected by z3 even when pipe-quoted. Prefix such
122+
names with `$` to make them valid simple symbols. -/
123+
def sanitizeSmtName (name : String) : String :=
124+
if name.isEmpty then name
125+
else
126+
let first := name.front
127+
if first == '@' || first == '.' then "$" ++ name else name
150128

151129
/-- The `$__` prefix is reserved for internal use and cannot appear in user
152-
identifiers (see `Strata.DL.Lambda.LState.EvalConfig.varPrefix`).
153-
The `.` after `t`/`f` prevents collision with Lambda-generated names
154-
like `$__t0` (variable `t`, index 0). -/
130+
identifiers. The `.` after `t`/`f` prevents collision with
131+
evaluation-generated names (which use `@N` suffixes). -/
155132
def termId (n : Nat) : String := s!"$__t.{n}"
156133
def ufId (n : Nat) : String := s!"$__f.{n}"
157134

@@ -189,10 +166,10 @@ def defineRecord (ty : TermType) (tEncs : List Term) : EncoderM Term := do
189166
def encodeUF (uf : UF) : EncoderM String := do
190167
if let (.some enc) := (← get).ufs.get? uf then return enc
191168
-- Check for name clashes with already-encoded UFs and reserved keywords, disambiguate
192-
let baseName := uf.id
193-
let existingNames := (← get).ufs.toList.map (·.2) |>.toArray
194-
let isUsed := fun candidate => existingNames.contains candidate || smtReservedKeywords.contains candidate
195-
let id := findUniqueName baseName 1 isUsed (existingNames.size + smtReservedKeywords.length)
169+
let baseName := sanitizeSmtName uf.id
170+
let existingNames := (← get).ufs.toList.map (·.2)
171+
let usedNames := Std.HashSet.ofList (existingNames ++ smtReservedKeywords)
172+
let id := Strata.Name.findUnique baseName 1 usedNames
196173
comment uf.id
197174
let argTys := uf.args.map (fun vt => vt.ty)
198175
Solver.declareFun id argTys uf.out

Strata/DL/SMT/Solver.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def comment (comment : String) : SolverM Unit :=
160160
emitln s!"; {inline}"
161161

162162
def getValue (ids : List String) : SolverM Unit :=
163-
let ids := Std.Format.joinSep ids " "
163+
let ids := Std.Format.joinSep (ids.map quoteIdent) " "
164164
emitln s!"(get-value ({ids}))"
165165

166166
def declareSort (id : String) (arity : Nat) : SolverM Unit :=

Strata/Languages/Core/Env.lean

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module
77

88
public import Strata.Languages.Core.Program
99
public import Strata.DL.Imperative.EvalContext
10+
public import Strata.Util.Name
1011

1112
public section
1213

@@ -244,23 +245,21 @@ def Env.addToContext
244245
: Env :=
245246
List.foldl (fun E (x, v) => E.insertInContext x v) E xs
246247

247-
-- TODO: prove uniqueness, add different prefix
248+
-- TODO: prove uniqueness
248249
def Env.genSym (x : String) (c : Lambda.EvalConfig CoreLParams) : CoreIdent × Lambda.EvalConfig CoreLParams :=
249-
let new_idx := c.gen
250-
let c := c.incGen
251-
let new_var := c.varPrefix ++ x ++ toString new_idx
250+
let (new_var, c) := c.genSym x
252251
(⟨new_var, ()⟩, c)
253252

254253
def Env.genVar' (x : String) (σ : (Lambda.LState CoreLParams)) :
255254
(CoreIdent × (Lambda.LState CoreLParams)) :=
256-
let (new_var, config) := Env.genSym x σ.config
255+
-- If `x` is already bound in the state, mark it used so that findUnique
256+
-- skips the bare name and avoids self-referential substitutions
257+
-- (e.g. havoc x generating fvar "x" when x is in scope).
258+
let config := if σ.state.find? (⟨x, ()⟩ : CoreIdent) |>.isSome
259+
then { σ.config with usedNames := σ.config.usedNames.insert x }
260+
else σ.config
261+
let (new_var, config) := Env.genSym x config
257262
let σ : Lambda.LState CoreLParams := { σ with config := config }
258-
-- let known_vars := Lambda.LState.knownVars σ
259-
-- if new_var ∈ known_vars then
260-
-- panic s!"[LState.genVar] Generated variable {Std.format new_var} is not fresh!\n\
261-
-- Known variables: {Std.format σ.knownVars}"
262-
-- else
263-
-- (new_var, σ)
264263
(new_var, σ)
265264

266265
def Env.genVar (x : Expression.Ident) (E : Env) : Expression.Ident × Env :=

Strata/Languages/Core/ProcedureEval.lean

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ The differences across paths are:
3939
clears `deferred` on the false branch, so pre-split obligations appear only
4040
in the first (true) path; post-split obligations appear in each path under
4141
distinct path conditions.
42-
- `exprEnv.config.gen`: may diverge when branches execute different numbers of
43-
`genFVar` calls (e.g. procedure calls only in one branch). We take the max to
44-
prevent fresh-variable name collisions in subsequent evaluation.
42+
- `exprEnv.config.usedNames`: may diverge when branches generate different
43+
variables. We union the sets to prevent fresh-variable name collisions in
44+
subsequent evaluation.
4545
4646
The `fallback` Env is returned when `results` is empty (which should not occur
4747
in practice, since `Statement.eval` always produces at least one result).
@@ -52,18 +52,32 @@ private def mergeResults (fallback : Env) (results : List Env) : Env :=
5252
| [E] => E
5353
| E :: rest =>
5454
let allDeferred := rest.foldl (fun acc e => acc ++ e.deferred) E.deferred
55-
let maxGen := rest.foldl (fun acc e => max acc e.exprEnv.config.gen) E.exprEnv.config.gen
55+
let mergedNames := rest.foldl (fun acc e =>
56+
acc.union e.exprEnv.config.usedNames) E.exprEnv.config.usedNames
5657
{ E with
5758
deferred := allDeferred,
58-
exprEnv := { E.exprEnv with config := { E.exprEnv.config with gen := maxGen } } }
59+
exprEnv := { E.exprEnv with config := { E.exprEnv.config with usedNames := mergedNames } } }
60+
61+
/--
62+
Evaluate a single procedure: generate fresh variables for parameters,
63+
execute the body, check postconditions, and collect proof obligations.
64+
-/
5965

6066
def eval (E : Env) (p : Procedure) : Env × Statistics :=
6167
-- Create a new scope with the formals and return variables. We will pop this
6268
-- scope at the end of this procedure.
69+
-- Parameters go through genFVars for globally unique names.
70+
-- Mark original parameter names as used so that fvar names always differ
71+
-- from the scope keys. Without this, a bare fvar "x" would be captured
72+
-- by the scope entry for "x" after reassignment, causing old(x) to
73+
-- resolve to the post-assignment value instead of the initial value.
6374
let vars := p.header.inputs.keys ++ p.header.outputs.keys
75+
let E := vars.foldl (fun E (v : CoreIdent) =>
76+
{ E with exprEnv.config.usedNames := E.exprEnv.config.usedNames.insert v.name }) E
6477
let var_tys := p.header.inputs.values ++ p.header.outputs.values
6578
let var_tys := var_tys.map (fun ty => some ty)
66-
let (vals, E) := E.genFVars (vars.zip var_tys)
79+
let vars_typed := vars.zip var_tys
80+
let (vals, E) := E.genFVars vars_typed
6781
let pVarMap := List.zip vars (var_tys.zip vals)
6882
let E := E.pushScope pVarMap
6983
let E := { E with pathConditions := E.pathConditions.push [] }

Strata/Languages/Core/SMTEncoder.lean

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -277,20 +277,17 @@ partial def toSMTTerm (E : Env) (bvs : BoundVars) (e : LExpr CoreLParams.mono) (
277277
let fvarNames := (e.collectFvarNames.map (·.name)).toArray
278278
-- Generate base name using global counter to ensure uniqueness across terms.
279279
-- The `$__` prefix is reserved for internal use and cannot appear in user
280-
-- identifiers (see `Strata.DL.Lambda.LState.EvalConfig.varPrefix`).
280+
-- identifiers.
281281
let (baseName, startSuffix) :=
282282
if ctx.uniqueBoundNames || name.isEmpty then
283283
(s!"$__bv{ctx.bvCounter}", 1)
284284
else
285-
Encoder.breakDisambiguatedName name
285+
let (b, s) := Strata.Name.breakDisambiguated name
286+
(Encoder.sanitizeSmtName b, s)
286287
let ctx := { ctx with bvCounter := ctx.bvCounter + 1 }
287288
-- Check for clashes with existing bvars, fvars in ctx, and fvars in body
288-
let isUsed := fun candidate =>
289-
bvs.any (fun (n, _) => n == candidate) ||
290-
ctx.ufs.any (fun uf => uf.id == candidate) ||
291-
fvarNames.contains candidate
292-
let limit := bvs.length + ctx.ufs.size + fvarNames.size
293-
let x := Encoder.findUniqueName baseName startSuffix isUsed limit
289+
let usedNames := Std.HashSet.ofList (bvs.map (·.1) ++ ctx.ufs.toList.map (·.id) ++ fvarNames.toList)
290+
let x := Strata.Name.findUnique baseName startSuffix usedNames
294291
let (ety, ctx) ← LMonoTy.toSMTType E ty ctx useArrayTheory
295292
let (trt, ctx) ← appToSMTTerm E ((x, ety) :: bvs) tr [] ctx useArrayTheory
296293
let (et, ctx) ← toSMTTerm E ((x, ety) :: bvs) e ctx useArrayTheory

Strata/Util/Name.lean

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/-
2+
Copyright Strata Contributors
3+
4+
SPDX-License-Identifier: Apache-2.0 OR MIT
5+
-/
6+
module
7+
public import Std.Data.HashSet.Basic
8+
9+
/-! # Name disambiguation utilities
10+
11+
Shared helpers for generating unique names. Bare names are preferred;
12+
`@N` suffixes are added only when disambiguation is needed.
13+
Used by the SMT encoder (for UF/bound-variable disambiguation) and
14+
the symbolic evaluator (for readable generated variable names).
15+
-/
16+
17+
public section
18+
19+
namespace Strata.Name
20+
21+
/-- Generate a disambiguated name by appending `@suffix`. -/
22+
def disambiguate (baseName : String) (suffix : Nat) : String :=
23+
s!"{baseName}@{suffix}"
24+
25+
/-- Parse a list of digit characters as a natural number. -/
26+
def digitsToNat (cs : List Char) : Nat :=
27+
cs.foldl (fun n c => n * 10 + (c.toNat - '0'.toNat)) 0
28+
29+
/-- Break a potentially disambiguated name into its base name and next suffix.
30+
If the name has an `@N` suffix, returns `(base, N + 1)`.
31+
Otherwise returns `(name, 1)`. -/
32+
def breakDisambiguated (name : String) : String × Nat :=
33+
let cs := name.toList
34+
let digitSuffix := cs.reverse.takeWhile Char.isDigit |>.reverse
35+
let rest := cs.reverse.dropWhile Char.isDigit |>.reverse
36+
match rest.reverse, digitSuffix with
37+
| '@' :: _, _ :: _ => (String.ofList rest.dropLast, digitsToNat digitSuffix + 1)
38+
| _, _ => (name, 1)
39+
40+
/-- Fast candidate search with a fuel counter. Returns `none` if fuel is exhausted. -/
41+
def findUniqueFast (baseName : String) (candidate : String) (suffix : Nat)
42+
(usedNames : Std.HashSet String) (fuel : Nat) : Option String :=
43+
if !usedNames.contains candidate then some candidate
44+
else match fuel with
45+
| 0 => none
46+
| fuel + 1 =>
47+
findUniqueFast baseName (disambiguate baseName suffix) (suffix + 1) usedNames fuel
48+
49+
/-- Provably-terminating fallback via list erasure.
50+
Uses `usedSet` (a `HashSet`) for O(1) membership checks and `remaining`
51+
(a list that shrinks via erasure) for termination.
52+
Returns `none` only if `remaining` is exhausted before finding a
53+
candidate outside `usedSet` — unreachable when `remaining` covers
54+
`usedSet` and candidates are distinct. -/
55+
def findUniqueSlow (baseName : String) (candidate : String) (suffix : Nat)
56+
(usedSet : Std.HashSet String) (remaining : List String) : Option String :=
57+
if !usedSet.contains candidate then some candidate
58+
else if h : remaining.contains candidate then
59+
have : (remaining.erase candidate).length < remaining.length := by grind
60+
findUniqueSlow baseName (disambiguate baseName suffix) (suffix + 1)
61+
usedSet (remaining.erase candidate)
62+
else none
63+
termination_by remaining.length
64+
65+
/-- Find a unique name by trying candidates with increasing `@N` suffixes.
66+
Uses a fast counter-based loop, falling back to a provably-terminating
67+
list-erasure search if the counter is exhausted (so we never panic). -/
68+
def findUnique (baseName : String) (startSuffix : Nat)
69+
(usedNames : Std.HashSet String) : String :=
70+
let firstCandidate :=
71+
if startSuffix == 1 then baseName
72+
else disambiguate baseName (startSuffix - 1)
73+
match findUniqueFast baseName firstCandidate startSuffix usedNames 1000000 with
74+
| some name => name
75+
| none =>
76+
let slowSuffix := startSuffix + 1000000
77+
let usedList := usedNames.toList
78+
match findUniqueSlow baseName (disambiguate baseName slowSuffix)
79+
(slowSuffix + 1) usedNames usedList with
80+
| some name => name
81+
| none => disambiguate baseName (slowSuffix + usedList.length + 1)
82+
83+
end Strata.Name
84+
85+
end -- public section

0 commit comments

Comments
 (0)