Zulip Chat Archive

Stream: lean4

Topic: Getting string expression out of String.mk app


Arthur Paulino (May 23 2022 at 23:48):

I have an expression that, when printed, outputs

String.mk (List.cons.{0} Char (Char.mk (UInt32.mk (Fin.mk UInt32.size 105 (_private.Init.Prelude.0.isValidChar_UInt32 (OfNat.ofNat.{0} Nat 105 (instOfNatNat 105)) (Or.inl (LT.lt.{0} Nat instLTNat (OfNat.ofNat.{0} Nat 105 (instOfNatNat 105) ...

It's a very long expression. How can I turn it into an Expr.lit (.strVal bla) ble?

Arthur Paulino (May 24 2022 at 00:05):

MWE:

import Lean

-- TODO: Fix documentation

-- Pretty much lifted from Hspec
inductive FailureReason
  | noReason
  | reason (descr : String := "")
  | unmatch (descr : String := "") (exp got : String)
  | error {ε α : Type} (descr : String := "") (ex : Except ε α)

def FailureReason.toString : FailureReason  String
  | .noReason              => "× Failure!"
  | .reason         descr  => descr
  | .unmatch descr exp got =>
    let ebg := s!"Expected '{exp}' but got '{got}'"
    if descr.isEmpty then ebg else s!"{descr}: {ebg}"
  | .error   descr except  =>
    if descr.isEmpty then "Exception thrown" else s!"{descr}"

inductive Result
  | ok   (successMessage : String := "✓ Success!")
  | fail (reason : FailureReason := .noReason) -- (Maybe Location)

def Result.toString : Result  String
  | .ok   msg => msg
  | .fail rsn => rsn.toString

-- helper function for now, but can very easily add more robust descriptions in the generic specs
-- below
def ofBool : Bool  Result
  | true  => .ok
  | false => .fail

def Result.toBool : Result  Bool
  | .ok _ => true
  | _     => false

-- I went back and forth on this for a while, and arrived at this tentative definition of a Spec.
structure SpecOn {α : Type} (obj : α) where
  -- Specs can contain parameters to allow for an eventual way of writing specs
  testParam : Type
  -- The actual property that's being tested
  -- I wanted this to be a literal `Prop`, but dealing with the `DecidablePred`
  -- instance was annoying
  prop : testParam  Result

@[reducible] def equals {α : Type} [BEq α] (a b : α) : SpecOn () :=
  Unit, fun _ => ofBool $ a == b

-- The idea is to write generic specs in the library like this one
@[reducible] def alwaysEquals {α β : Type} [BEq β] (f : α  β) (b : β) : SpecOn f :=
  α, fun a => ofBool $ f a == b

-- Specs can also not contain parameters if they're specs about things that don't fit neatly into
-- a function type
@[reducible] def doesntContain {β : Type} [BEq β] (bx : List β) : SpecOn bx :=
  β, fun b => ofBool $ not $ bx.contains b

@[reducible] def depDoesntContain {α β : Type} [BEq β] (f : α  List β) : SpecOn f :=
  α × β, fun (a, b) => ofBool $ not $ (f a).contains b

@[reducible] def neverContains {α β : Type} [BEq β] (f : α  List β) (b : β) : SpecOn f :=
  α, fun a => ofBool $ not $ (f a).contains b

section SectionExample

variable {α : Type} {a : α}

-- Basic Example type, as functionality is added it will probably get more complicated (custom messages
-- and configurations per example)
structure ExampleOf (spec : SpecOn a) where
  descr : Option String
  exam  : spec.testParam

abbrev ExamplesOf (spec : SpecOn a) := List $ ExampleOf spec

namespace ExampleOf

-- Tool to construct "default" examples from a given parameter, this will be helpful eventually when
-- examples become more complicated
def fromParam {spec : SpecOn a} (input : spec.testParam) : ExampleOf spec :=
  none, input

def fromDescrParam {spec : SpecOn a} (descr : String) (input : spec.testParam) : ExampleOf spec :=
  descr, input

-- Check the example, and get a `Result`
def check {α : Type} {a : α} {spec : SpecOn a} (exmp : ExampleOf spec) : Result :=
  spec.prop exmp.exam

-- This can eventually be expanded so a run does more than just IO
def run {α : Type} {a : α} {spec : SpecOn a} (exmp : ExampleOf spec) : Bool × String :=
  let res := exmp.check
  let msg : String := match exmp.descr with
    | none   => res.toString
    | some d => s!"it {d}: {res.toString}"
  (res.toBool, msg)

end ExampleOf

-- Ditto from above
namespace ExamplesOf

def fromParams {α : Type} {a : α} {spec : SpecOn a} (input : List spec.testParam) : ExamplesOf spec :=
  input.map <| .fromParam

def check {α : Type} {a : α} {spec : SpecOn a} (exmp : ExamplesOf spec) : List Result :=
  exmp.map ExampleOf.check

def run {α : Type} {a : α} {spec : SpecOn a} (exmps : ExamplesOf spec) : List (Bool × String) :=
  exmps.map ExampleOf.run

end ExamplesOf

end SectionExample

open Lean

def getBool! : Expr  Bool
  | .const ``Bool.true  .. => true
  | .const ``Bool.false .. => false
  | _                      => unreachable!

def getStr! : Expr  String
  | .lit (.strVal s) _ => s
  | _                  => panic! "not Expr.lit!"

def recoverTestResult (res : Expr) : Bool × String :=
  (getBool! $ res.getArg! 2, getStr! $ res.getArg! 3)

open Meta Elab Command Term in
elab "#spec " term:term : command =>
  liftTermElabM `assert do
    let term  elabTerm term none
    synthesizeSyntheticMVarsNoPostponing
    let type  inferType term
    if type.isAppOf ``ExampleOf then
      -- a `Bool × String` tuple:
      let res  reduce ( mkAppM ``ExampleOf.run #[term])
      dbg_trace res.getArg! 3
      let (success?, msg) := recoverTestResult res
      if success? then
        logInfo msg
      else
        throwError msg
    else if type.isAppOf ``ExamplesOf then
       -- a list of `Bool × String`
      let res  reduce ( mkAppM ``ExamplesOf.run #[term])
      match res.listLit? with
      | none => unreachable!
      | some (_, res) =>
        let res := res.map recoverTestResult
        let success? := res.foldl (init := true) fun acc (b, _) => acc && b
        let msg' : String := "\n".intercalate $ res.map
          fun (_, msg) => msg
        if success? then
          logInfo msg'
        else
          throwError msg'
    else throwError "Invalid term to run '#spec' with"

def foo (n : Nat) : Nat := n

-- Once we have generic specs above, we can easily construct specs for particular examples
-- The idea is to hook this into a version of the syntax Arthur implemented in `YatimaSpec.lean`
@[reducible] def fooSpec : SpecOn foo := alwaysEquals foo 4

-- Can create examples for the specs also using .fromParam
def fooExample  : ExampleOf fooSpec := .fromDescrParam "this message" 4
def fooExamples : ExamplesOf fooSpec := .fromParams [2,3,4,5,6,6]

#spec fooExample

Arthur Paulino (May 24 2022 at 00:07):

I was expecting it to output "this message" as a successful test, but it's panicking instead. I'm using dbg_trace to print out the expression that's being passed to the getStr! function

Gabriel Ebner (May 24 2022 at 07:52):

From what I can tell, you want #spec to evaluate the argument (like #eval) does. For that you shouldn't use reduce (which uses the kernel reduction), but rather evalExpr (which uses the VM/native code). There's a barebones version in core and a more featureful one in mathlib.

Arthur Paulino (May 24 2022 at 10:24):

Thanks!! Will try soon :pray:

Arthur Paulino (May 24 2022 at 15:11):

@Gabriel Ebner how to use that function? it seems like it's doing stuff with the environment, which is a bit weird

Gabriel Ebner (May 24 2022 at 18:13):

import Mathlib.Util.Eval

open Lean

#eval Mathlib.Eval.evalExpr
        Nat -- result type
        (mkConst ``Nat) -- result type as Expr
        (mkConst ``Nat.zero) -- thing to evaluate

#eval Lean.Elab.Term.evalExpr
        Nat -- result type
        ``Nat -- name of result type (needs to be top-level definition)
        (mkConst ``Nat.zero) -- thing to evaluate

Last updated: Dec 20 2023 at 11:08 UTC