LemmaScript

July 27, 2026 · View on GitHub

Version: 0.5.19 Date: July 2026

This document covers what is unique to the Dafny backend. See SPEC.md for the shared annotation language, translation rules, type mapping, and pipeline.


1. Project Structure

For each verified TS function foo.ts, there are two Dafny files:

FileWho writes itPurpose
foo.dfy.genlsc genGenerated from TS. Always regeneratable. Merge base.
foo.dfyLLM / UserStarts as copy of gen. Proof annotations added here. Source of truth.

The .dfy.gen extension prevents Dafny tooling from auto-verifying it.

The diff between gen and dfy must be additions only — the LLM may insert helper lemmas, ghost predicates, assert statements, and loop invariants, but may not modify generated lines.


2. Pure Functions

Pure TS functions become Dafny function declarations (no wrapper, no namespace). requires and ensures are emitted directly. If the function has ensures, a companion lemma is generated as a proof target for the LLM:

function clamp(v: int, lo: int, hi: int): int
  requires lo <= hi
{
  if v < lo then lo
  else if v > hi then hi
  else v
}

lemma clamp_ensures(v: int, lo: int, hi: int)
  requires lo <= hi
  ensures clamp(v, lo, hi) >= lo
  ensures clamp(v, lo, hi) <= hi
{
}

Non-pure functions become Dafny method declarations.


3. Regeneration Workflow

lsc regen --backend=dafny foo.ts:

  1. Read old foo.dfy.gen before overwriting
  2. Regenerate foo.dfy.gen
  3. If foo.dfy doesn't exist → create from gen, verify, done
  4. If gen changed → three-way merge (git merge-file) using old gen as base
  5. Check additions-only invariant
  6. Verify merged foo.dfy
  7. On success, delete .dfy.base (gen is now the anchor)

On merge conflict, the original foo.dfy is restored and the merged result is saved as foo.dfy.merged for manual inspection.


4. Helper Preambles

The Dafny emitter auto-injects helper functions when needed. Each is emitted at most once, only when a construct that requires it appears (registry: PREAMBLE_CODE in dafny-emit.ts).

Core:

HelperWhenPurpose
Option<T>Map.get, optional typesdatatype Option<T> = None | Some(value: T)
SetToSeqfor (x of set), map/record iterationConvert set to sequence for iteration
SetFromSeqnew Set(arr)Build a deduplicated set from a sequence (set x | x in s)

Numeric:

HelperWhenPurpose
JSFloorDivMath.floor(a/b) (int args)JS-compatible floor division
FloorReal / CeilRealMath.floor(x) / Math.ceil(x) (real arg)real → int via .Floor
MathAbs / MathMin / MathMaxMath.abs/min/max(a, b)Scalar abs/min/max
MaxOfSeq / MinOfSeqMath.max(...s) / Math.min(...s)Aggregate over a sequence (requires |s| > 0)
Pow2 / BitAnd<< / >> / & on bigintBitwise ops as arithmetic
NatToString / IntToString`${n}` template literal (nat / signed int)Number-to-digit-string for interpolation (IntToString prefixes -)

Sequence:

HelperWhenPurpose
SeqIndexOfarr.indexOf(x)First-index search (-1 if absent)
SeqFindIndexarr.findIndex(f)Predicate first-index search
SeqFindarr.find(f)Predicate first-match search
SeqFindLastarr.findLast(f)Predicate last-match search
SeqFilterSomefilterMap pattern (§3.7)Drop Nones and unwrap to seq<T>
SeqFlattenarr.flat()Flatten one level
SeqJoinarr.join(sep)Join into a string
SafeSlicearr.slice(lo, hi) under //@ safe-sliceBounds-clamping slice
Permperm(a, b) (spec-only)predicate Perm<T(==)>(a, b) { multiset(a) == multiset(b) }

String:

HelperWhenPurpose
StringIndexOfs.indexOf(sub), s.indexOf(sub, from), s.includes(sub)Recursive string search (also provides StringIndexOfFrom)
StringSplits.split(d)Axiomatic split (1 <= |res| <= |s| + 1)
StringTrims.trim() / s.trimEnd() / s.trimStart()Trim (also provides StringTrimRight / StringTrimLeft); strips the full ECMAScript whitespace set via IsJSWhitespace, not just ' '
StringToLower / StringToUppers.toLowerCase() / s.toUpperCase()Case folding

5. Verification

lsc check --backend=dafny foo.ts:

  1. Generate foo.dfy.gen + seed foo.dfy
  2. Check additions-only invariant
  3. Run dafny verify foo.dfy

Standard libraries are auto-detected: if foo.dfy contains import Std., the --standard-libraries flag is added.

The shared --time-limit=<seconds> flag (SPEC.md §7) maps to Dafny's --verification-time-limit; --extra-flags=<string> is forwarded verbatim to dafny verify.