Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
502 changes: 502 additions & 0 deletions rocq-ltac2-tc-dispatch/LICENSE-LGPL

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions rocq-ltac2-tc-dispatch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Extensible Ltac2 Dispatch with Typeclasses
===========================================

To use the library, require via the following command:
```coq
Require Import skylabs.ltac2.tc_dispatch.lookup.
```
16 changes: 16 additions & 0 deletions rocq-ltac2-tc-dispatch/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
(env
(sl_timing
(binaries
(../rocq-tools/bin/rocq_perf.exe as rocq))
(flags :standard %{read-lines:flags/ocamlc})
(ocamlopt_flags :standard -O3 -unbox-closures)
(rocq
(rocqdep_flags :standard -w +all)
(flags (:standard %{read-lines:flags/coqc}))))
; Same, but without coqc_perf
(_
(flags :standard %{read-lines:flags/ocamlc})
(ocamlopt_flags :standard -O3 -unbox-closures)
(rocq
(rocqdep_flags :standard -w +all)
(flags (:standard %{read-lines:flags/coqc})))))
22 changes: 22 additions & 0 deletions rocq-ltac2-tc-dispatch/dune-project
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
(lang dune 3.21)
(using rocq 0.11)
(name ltac2-tc-dispatch)

(generate_opam_files)

(maintainers
"Gregory Malecha <gregory@skylabs-ai.com>")

(authors
"Gregory Malecha <gregory@skylabs-ai.com>")

(license MIT)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is there a LICENSE-LGPL file then?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We should change to LGPL to encourage contributions over forking.


(source (github SkyLabsAI/BRiCk))

(package
(name ltac2-tc-dispatch)
(synopsis "Typeclass-based lookup for Ltac2 functions")
(depends coq-core coq-stdlib stdlib-extra rocq-extra))

(subst disabled)
1 change: 1 addition & 0 deletions rocq-ltac2-tc-dispatch/flags
31 changes: 31 additions & 0 deletions rocq-ltac2-tc-dispatch/ltac2-tc-dispatch.opam
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
synopsis: "Typeclass-based lookup for Ltac2 functions"
maintainer: ["Gregory Malecha <gregory@skylabs-ai.com>"]
authors: ["Gregory Malecha <gregory@skylabs-ai.com>"]
license: "MIT"
homepage: "https://github.com/SkyLabsAI/BRiCk"
bug-reports: "https://github.com/SkyLabsAI/BRiCk/issues"
depends: [
"dune" {>= "3.21"}
"coq-core"
"coq-stdlib"
"stdlib-extra"
"rocq-extra"
"odoc" {with-doc}
]
build: [
[
"dune"
"build"
"-p"
name
"-j"
jobs
"@install"
"@runtest" {with-test}
"@doc" {with-doc}
]
]
dev-repo: "git+https://github.com/SkyLabsAI/BRiCk.git"
x-maintenance-intent: ["(latest)"]
4 changes: 4 additions & 0 deletions rocq-ltac2-tc-dispatch/plugin/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(library
(name ltac2_tc_dispatch)
(public_name ltac2-tc-dispatch.plugin)
(libraries coq-core.plugins.ltac2 rocq-extra stdlib-extra ltac2-extra.plugin))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
(libraries coq-core.plugins.ltac2 rocq-extra stdlib-extra ltac2-extra.plugin))
(libraries rocq-runtime.plugins.ltac2 rocq-extra stdlib-extra ltac2-extra.plugin))

47 changes: 47 additions & 0 deletions rocq-ltac2-tc-dispatch/plugin/lookup.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@

open Ltac2_plugin
open Tac2ffi
(* open Tac2val *)
open Tac2externals

let resolve_ltac2_safe (mp : string list) (id : string) : (unit -> unit Proofview.tactic) option =
let qualid = Libnames.make_qualid
(Names.DirPath.make (List.map Names.Id.of_string mp))
(Names.Id.of_string id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why not take a single string (containing .) and call qualid_of_string on it instead of taking string list + string and calling make_qualid?
That way conversion is simpler (no list_of_list_constr) and you don't have to remember which order the list needs to be in.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This makes sense. Alternatively, I was thinking that we could get the module path from the constant of the type class instance. That makes it less fragile to move things around.

in
let is_unit_typ (t : int Tac2expr.glb_typexpr) : bool =
(* TODO: why does Init.unit have two different representations? *)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

because you don't normalize it (cf what Tac2typing_env.kind does)

match t with
| Tac2expr.GTypRef (Tac2expr.Other ty, []) ->
ty = Tac2core.Core.t_unit
| Tac2expr.GTypRef (Tac2expr.Tuple 0, []) ->
true
| _ -> false
in
let is_unit_to_unit ((n, t) : Tac2expr.type_scheme) : bool =
Int.equal n 0 &&
match t with
| Tac2expr.GTypArrow (t1, t2) -> is_unit_typ t1 && is_unit_typ t2
| _ -> false
in
match Tac2env.locate_ltac qualid with
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I guess we don't expose the full_path based API but if we did would you want to use it?
The difference with locate is that locate will resolve short names, so for instance locate on case will succeed when Control is imported but the full path based API would need Ltac2.Control.case regardless of importing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For this use case, I would want to use definite references. Resolution at this level seems very awkward.

| Tac2expr.TacConstant kn ->
let data = Tac2env.interp_global kn in
if is_unit_to_unit data.Tac2env.gdata_type
then
let v = Tac2interp.eval_global kn in
Some (repr_to (fun1 unit unit) v)
else
None
| Tac2expr.TacAlias _ ->
None
| exception Not_found -> None


let define s =
define Tac2expr.{ mltac_plugin = "ltac2_tc_dispatch"; mltac_tactic = s }

let () =
define "resolve_ltac2"
(list string @-> string @-> ret (option (fun1 unit unit))) @@ fun modpath name ->
resolve_ltac2_safe modpath name
Empty file.
4 changes: 4 additions & 0 deletions rocq-ltac2-tc-dispatch/tests/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(include_subdirs qualified)
(rocq.theory
(name skylabs_tests.ltac2.tc_dispatch)
(theories Stdlib skylabs.ltac2.extra skylabs.ltac2.tc_dispatch))
13 changes: 13 additions & 0 deletions rocq-ltac2-tc-dispatch/tests/external.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Require Import Stdlib.Strings.PrimString.
Require Import Ltac2.Ltac2.
Require Import skylabs.ltac2.tc_dispatch.lookup.

Ltac2 ext_tactic () := ltac1:(reflexivity).

#[local] Open Scope pstring_scope.

#[export]
Instance nested_inst : Ltac2Lookup (0 = 0) := {
ltac2_path := "external" :: "tc_dispatch" :: "ltac2" :: "skylabs_tests" :: nil;
ltac2_name := "ext_tactic";
}.
17 changes: 17 additions & 0 deletions rocq-ltac2-tc-dispatch/tests/nested.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Require Import skylabs.ltac2.tc_dispatch.lookup.
Require Import Stdlib.Strings.PrimString.

#[local] Open Scope pstring_scope.

Module Import Nested.
Ltac2 inside_module () := ltac1:(reflexivity).
Instance nested_inst : Ltac2Lookup (0 = 0) := {
ltac2_path := "Nested" :: "nested" :: "tc_dispatch" :: "ltac2" :: "skylabs_tests" :: nil;
ltac2_name := "inside_module";
}.
End Nested.

Example test_success_nested : 0 = 0.
Proof.
goal_dispatch. (* Should resolve Nested.inside_module *)
Qed.
47 changes: 47 additions & 0 deletions rocq-ltac2-tc-dispatch/tests/simple.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Require Import Stdlib.Lists.List.
Require Import Stdlib.Strings.PrimString.
Require Import skylabs.ltac2.tc_dispatch.lookup.

#[local] Open Scope pstring_scope.

(* A simple success tactic *)
Ltac2 solve_true () := ltac1:(exact I).

(* A tactic that prints a message *)
Ltac2 say_hello () := Message.print (Message.of_string "Hello from dynamic lookup!").

(* Success Instance for True *)
Instance test_true_inst : Ltac2Lookup True := {
ltac2_path := "simple" :: "tc_dispatch" :: "ltac2" :: "skylabs_tests" :: nil;
ltac2_name := "solve_true";
}.

(* Success Instance for False (pointing to a printing tactic) *)
Instance test_hello_inst : Ltac2Lookup False := {
ltac2_path := nil;
ltac2_name := "say_hello";
}.

(* A "Broken" Instance: String name does not exist *)
Instance broken_inst : Ltac2Lookup (True -> True) := {
ltac2_path := nil;
ltac2_name := "non_existent_tactic_12345";
}.

Example test_succeed : True.
Proof.
goal_dispatch.
Abort.


(* Testing the "No Instance" failure *)
Example test_fail_no_instance : unit.
Proof.
Fail goal_dispatch.
Abort.

(* Testing the "Invalid Name" failure *)
Example test_fail_bad_string : nat.
Proof.
Fail goal_dispatch. (* Triggers the None branch of our match *)
Abort.
7 changes: 7 additions & 0 deletions rocq-ltac2-tc-dispatch/tests/test_external.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Require Import skylabs_tests.ltac2.tc_dispatch.external.
Require Import skylabs.ltac2.tc_dispatch.lookup.

Goal 0 = 0.
Proof.
goal_dispatch.
Qed.
22 changes: 22 additions & 0 deletions rocq-ltac2-tc-dispatch/theories/crush_ext.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Require Ltac2.Ltac2.
Require Import skylabs.ltac2.tc_dispatch.lookup.

(**
[crush_ext] is An extensible version of [crush].
Users can add new tactics / strategies by adding [Ltac2Lookup] hints
to the [crush_ext] database.
*)

Create HintDb crush_ext discriminated.

Module ltac2.
Import Ltac2.Init.
Import Ltac2.Notations.
Ltac2 crush2 () :=
let dbs := Some [ident:(crush_ext)] in
repeat (ltac2.goal_dispatch_with dbs).
End ltac2.

Ltac crush_ext :=
ltac2:(ltac2.crush2 ()).
10 changes: 10 additions & 0 deletions rocq-ltac2-tc-dispatch/theories/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
(include_subdirs qualified)
(rocq.theory
(name skylabs.ltac2.tc_dispatch)
(package ltac2-tc-dispatch)
(theories
Ltac2
Stdlib ; for ltac2-extra/theories/internal/char.v
skylabs.ltac2.extra
)
(plugins ltac2-tc-dispatch.plugin))
76 changes: 76 additions & 0 deletions rocq-ltac2-tc-dispatch/theories/lookup.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
(*
* Copyright (C) 2026 SkyLabs AI, Inc.
*)
Require Import Ltac2.Ltac2.
Require Import Ltac2.Control.
Require Import Ltac2.Printf.


Require Import Ltac2.Std.
Require Import Stdlib.Strings.PrimString.
Require Import skylabs.ltac2.extra.extra.

Declare ML Module "ltac2-tc-dispatch.plugin".

(* The typeclass: P is the goal/type, path is the module hierarchy, name is the tactic *)
Class Ltac2Lookup (P : Prop) := {
ltac2_path : list string;
ltac2_name : string;
}.
Comment on lines +15 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess we could add a new vernacular command for populating this class, and also checking the type of the given Ltac2 value.


Module ltac2.

Ltac2 @ external resolve_ltac2 : string list -> string -> (unit -> unit) option :=
"ltac2_tc_dispatch" "resolve_ltac2".

Ltac2 Type exn ::= [ UnexpectedConstr (string * constr) ].

Ltac2 string_of_pstring_constr (c : constr) : string :=
match Constr.Unsafe.kind c with
| Constr.Unsafe.String s => Pstring.to_string s
| _ => throw (UnexpectedConstr("Expected a pstring", c))
end.

Ltac2 rec list_of_list_constr (f : constr -> 'a) (c : constr) : 'a list :=
lazy_match! c with
| List.nil => []
| List.cons ?c ?cs => f c :: list_of_list_constr f cs
end.

Ltac2 goal_dispatch_with (dbs : ident list option) :=
let g := Control.goal () in
let query := constr:(Ltac2Lookup $g) in
(* let _ := printf "query=%t" query in *)
let inst := Constr.Unsafe.make (TC.resolve dbs query) in
(* let _ := printf "inst=%t" inst in *)
let flags := RedFlags.all in
let reduced_inst := Std.eval_cbv flags inst in
(* let _ := printf "reduced_inst=%t" reduced_inst in *)

lazy_match! reduced_inst with
| {| ltac2_path := ?p; ltac2_name := ?n |} =>
let p_ltac2 := list_of_list_constr (fun c => string_of_pstring_constr c) p in
let n_ltac2 := string_of_pstring_constr n in
(* printf "n_ltac2=%s" n_ltac2 ; *)

match resolve_ltac2 p_ltac2 n_ltac2 with
| Some f =>
(* let _ := printf "resolve_ltac2 success!" in *)
f ()
| None =>
(* let _ := printf "resolve_ltac2 failed!" in *)

let err :=
Message.concat [Message.of_string "Could not find: ";
Message.of_string n_ltac2] in
Control.zero (Tactic_failure (Some err))
end
| _ =>
Control.zero (Tactic_failure (Some (Message.of_string "Could not reduce instance to record.")))
end.

End ltac2.

Ltac goal_dispatch :=
idtac;
ltac2:(ltac2.goal_dispatch_with None).