Skip to content

Laurel Language Enhancements#385

Open
fabiomadge wants to merge 299 commits intomainfrom
jverify-strata-backend
Open

Laurel Language Enhancements#385
fabiomadge wants to merge 299 commits intomainfrom
jverify-strata-backend

Conversation

@fabiomadge
Copy link
Contributor

@fabiomadge fabiomadge commented Feb 4, 2026

Summary

Expands the Laurel language with additional operators, data types, and verification constructs. Improves formatting across multiple dialects.

Key Changes

Laurel Grammar & Translator

  • New operators: -, *, /, %, /t, %t (truncating), ==>, !, unary -
  • Array support: Array<T> type, indexing, length
  • Sequence operations: Seq.From, Seq.Take, Seq.Drop, Seq.Contains
  • Constrained types with constraint injection into quantifiers
  • Quantifiers: forall, exists
  • Return statement works with postconditions
  • While loops with multiple invariants
  • Multiple requires clauses per procedure

Infrastructure

  • NewlineSepBy separator type for formatting preservation
  • Java codegen: list separator preservation, SourceRange overloads
  • Improved pretty printing for C_Simp, B3, Core

CLI Commands

  • laurelParse, laurelAnalyze, laurelToCore, laurelPrint

Bug Fixes

  • Fixed SMT encoding for multi-argument functions and Map type
  • Added substFvarLifting for proper de Bruijn index handling

Tests

  • Reorganized Laurel tests T01-T17
  • Added regression test for assignment lifting

Resolve conflicts from c0409bb 'Add tactic for simplifying termination proofs':
- Use term_by_mem where applicable in Laurel termination proofs
- Keep manual proofs where term_by_mem can't handle the goal
  (HighType recursion, attach patterns)
inputs : List Parameter
outputs : List Parameter
precondition : WithMetadata StmtExpr
determinism : Determinism
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened to determinism?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally removed in 67931730 — it was only ever set to .nondeterministic and never pattern-matched on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it wasn't used but that doesn't mean it shouldn't be there. The whole AST wasn't used when it was first merged. Can you bring it back?


theorem WithMetadata.sizeOf_val_lt {t : Type} [SizeOf t] (e : WithMetadata t) : sizeOf e.val < sizeOf e := by
cases e; grind
cases e <;> simp_wf <;> omega
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge accident?

Copy link
Contributor Author

@fabiomadge fabiomadge Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional. omega is simpler and more predictable than grind here, and it proves the goal.

| _ => none

/-- Collect Array.Get accesses with constrained element types -/
def collectConstrainedArrayAccesses (env : TypeEnv) (tcMap : TranslatedConstraintMap) (e : StmtExprMd) : List (StmtExprMd × StmtExprMd × TranslatedConstraint) :=
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the implementation of constraints be independent of the implementation of Arrays? If this is temporary code, can we mark it as such?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a TODO. Array.Get is special because it's a built-in (SMT select), not a procedure call, so there's no contract to assume from. Other constrained type sources (procedure returns, field reads) get their assumes via contracts. If more built-ins return constrained types in the future, this should be generalized.

let belse ← match elseBranch with
| some e => do let (_, s) ← translateStmt ctMap tcMap env outputParams postconds e; pure s
| none => pure []
pure (env, arrayElemAssumes ++ [Imperative.Stmt.ite bcond bthen belse stmt.md])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the translation of IfThenElse refer to arrayElemAssumes? That can't be right

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in fb9c7f60. Compound statements (Block, IfThenElse, While) no longer generate arrayElemAssumes at the top level. Their sub-statements handle their own assumes via recursive translateStmt calls.

(e', acc ++ ss)) (env, [])
(env', stmtsList)
| .LocalVariable name ty initializer =>
def translateStmt (ctMap : ConstrainedTypeMap) (tcMap : TranslatedConstraintMap) (env : TypeEnv) (outputParams : List Parameter) (postconds : List (String × Core.Expression.Expr)) (stmt : StmtExprMd) : Except String (TypeEnv × List Core.Statement) := do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you introduce assert statements related to postconditions? Core will already check that procedure bodies adhere to the postconditions of that procedure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These asserts are needed for early returns. Laurel's return is translated to result := value; assert postconditions; assume false. The assume false prevents fall-through but also makes the path vacuously true, so Core's VCG won't check postconditions on it.

Without the asserts, a postcondition-violating early return passes verification silently. I verified this by writing a Core program where the early return path sets result := -x (violating ensures result >= 0):

  • Without the assert before assume false: ✅ pass (unsound)
  • With the assert before assume false: ❌ fail (correct)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Laurel's return is translated to result := value; assert postconditions; assume false

Is that new? It's not mentioned in the PR. I think it would be better to encode a return using a jump than an assume false. With the assume false it would be incorrect when executing the code, since then the assume is ignored, and having to assert the postconditions introduces a lot of duplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that goto would be better. I tried implementing it (return valueresult := value; goto "\$return" with \$return: {} at the end of the body), and it works correctly for individual procedures.

However, I found a Core VCG issue: when a procedure has a goto inside an if branch, processIteBranches produces multiple EnvWithNext results, and these multiply across procedures via ProgramEval.flatMap. A second procedure's assertions get checked N times instead of once.

Filed as #419 with a proposed fix (merge paths by nextLabel in processIteBranches). Keeping assume false for now until that's resolved.

Copy link
Contributor

@keyboardDrummer keyboardDrummer Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I found a Core VCG issue

Nice find! How did you find that?

I'm hesitant to merge the assert+assume solution for reasons that I think you identified as well:

  • It works with an analysis that respects assume false but not one that ignores it, like concrete execution.
  • It duplicates the postconditions for each return so it breaks our goal of having transformations that are linear in size.

The current PythonToCore translation uses the goto approach, so to support moving that over to Laurel, we could use the goto approach as well.

I'm also ok with using the assert+assume approach if we sufficiently document that it is a temporary solution.

fabiomadge and others added 5 commits February 12, 2026 21:06
…aints

- Don't generate arrayElemAssumes for compound statements (Block, IfThenElse,
  While). Their sub-statements handle their own assumes via recursive
  translateStmt calls. Previously, assumes for array accesses inside branches
  were incorrectly hoisted before the if/while statement.
- Add TODO comment explaining why constraint injection is array-specific:
  Array.Get is a built-in (SMT select), not a procedure call, so there's
  no contract to assume from.
Copy link
Contributor

@joehendrix joehendrix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a few suggestions, but this looks reasonable to me. I'll approve this if you don't like the suggestions.

- Parser.lean: use Id.run do + early return for comment parsing, replace
  startsWith with utf8ByteSize check (comment 6)
- Java/Gen.lean: match on DDM category name (Init.Num/Init.Decimal) instead
  of Java type string for builder parameter generation (comment 7)
- Format.lean: collapse comma/space/newline seq formatting into single
  sepBy arm, eliminating duplication (comment 8)
- Java/Gen.lean: consolidate getSeparator with SepFormat.toIonName by
  adding fromCategoryName? and moving Ion-specific functions to
  Strata.DDM.Ion (comment 9)
@fabiomadge fabiomadge force-pushed the jverify-strata-backend branch from 80b2cc2 to 88d1a43 Compare February 13, 2026 02:21
@@ -1 +1 @@
/-
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the rename of this test?

body := body
}) .empty

def arrayTypeSynonym : Core.Decl := .type (.syn { name := "Array", typeArgs := ["T"], type := .tcons "Map" [.int, .ftvar "T"] })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move these static Core definition to CorePrelude.lean ?

explicitPreconditions := explicitPreconditions ++ [(s!"{proc.name}_pre_{i}", check)]
let preconditions := inputConstraints ++ arrayLenConstraints ++ explicitPreconditions
-- Generate constraint checks for output parameters with constrained types
let outputConstraints : List (Core.CoreLabel × Core.Procedure.Check) ←
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you generate the implicit pre and postconditions in a separate pass in a separate file?

| some expr => pure (some (s!"{proc.name}_input_{p.name}_constraint", { expr, md := p.type.md }))
| none => pure none)
-- Array lengths are implicitly >= 0
let arrayLenConstraints : List (Core.CoreLabel × Core.Procedure.Check) :=
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move everything related to arrays to a separate file? Can you elaborate on the design of Array support? How come it's not a Laurel library? The length constraint seems like a type invariant or constrained type.


/-- Extract sequence bounds from Seq.From/Take/Drop chain.
Note: Seq.From only works with simple Identifier arguments; complex expressions are not supported. -/
def translateSeqBounds (env : TypeEnv) (expr : StmtExprMd) : Except String SeqBounds :=
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can everything related to sequences be in a separate file? Can it be a separate pass?

@@ -0,0 +1,42 @@
/-
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move and Sequence support tests into a separate folder?

@keyboardDrummer
Copy link
Contributor

Can you add Laurel test Idiomaticity.lean that compares it's expectation against the generated Core code? That way it'll be easier to see how idiomatic the generated code is. I'm asking for this in this PR because some of the changes are pretty complicated and it's difficult for me to see from the translations what the end result is.

match outputParams.head? with
| some _ => do
let coreExpr ← translateExpr ctMap tcMap env stmt
mkReturnStmts (some coreExpr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the scenario where we return "naturally" because we reach the end of a block? Since this is not a jump, should we not call something different than mkReturnStmt which does do a jump?

op assume (cond : StmtExpr) : StmtExpr => "assume " cond ";";
op return (value : StmtExpr) : StmtExpr => "return " value ";";
op block (stmts : Seq StmtExpr) : StmtExpr => @[prec(1000)] "{" stmts "}";
op block (stmts : NewlineSepBy StmtExpr) : StmtExpr => @[prec(1000)] "{" indent(2, "\n" stmts) "\n}";
Copy link
Contributor

@keyboardDrummer keyboardDrummer Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indent(2, is never used anywhere, right? I don't think we're ever printing Laurel through the DDM. Why did you add this?

return commands
let (some tree, true) ← runChecked <| elabCommand leanEnv
| return commands
-- Prevent infinite loop if parser makes no progress
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great!!

| q`Init.SpacePrefixSepBy, 1 =>
let inner := mkCApp ``Array #[args[0]]
return if addAnn then mkCApp ``Ann #[inner, annType] else inner
| q`Init.NewlineSepBy, 1 =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you say more about this NewlineSepBy operator? What is it for?

github-merge-queue bot pushed a commit that referenced this pull request Feb 16, 2026
Contains a subset of the changes from
#385

### Changes
- Fix to Strata/DDM/Parser.lean so it can handle parsing comments `//`
in languages that have a division operator
- New operators: `-`, `*`, `/`, `%`, `/t`, `%t` (truncating), `==>`,
`!`, unary `-`
- Quantifiers: `forall`, `exists`
- While loops with multiple invariants

### Testing
- Added tests for the new features

---------

Co-authored-by: Andrew Wells <130512013+andrewmwells-amazon@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants