Skip to content

Add simple_word_enum command to performantly register large flat enum types - #266

Merged
ike-mulder-aws merged 5 commits into
awslabs:mainfrom
ike-mulder-aws:simple-word-enum-3
Aug 6, 2026
Merged

Add simple_word_enum command to performantly register large flat enum types#266
ike-mulder-aws merged 5 commits into
awslabs:mainfrom
ike-mulder-aws:simple-word-enum-3

Conversation

@ike-mulder-aws

Copy link
Copy Markdown
Collaborator

Add simple_word_enum command to create an enum type backed by words. For example,

simple_word_enum (32) my_enum urust:"MyEnum" =
            Answer = \<open>0x42\<close>
          | Best   = \<open>0x72\<close>

creates an Isabelle type my_enum, with variants Answer and Best, with conversion functions my_enum_to_u32_pure : my_enum => 32 word and my_enum_try_from_u32_pure : 32 word => (my_enum, unit) result, uRust versions of these, properties of these conversion functions, support for case analysis on my_enum, support for code extraction, and registers uRust notation MyEnum::Answer.

The only slow proof in this construction is distinct (variant_representation_values), which is quadratic. Tests show that enums with 128 variants take about ~5s locally to construct, which is a drastic improvement over Isabelle's native datatype and for the enum command already available in AutoCorrode. On the other hand, simple_word_enum can only handle flat enums.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Num_Case_Expression imports only Main and provides the `ncase _ of _`
selector, which is generic infrastructure rather than anything specific to
the shallow Micro Rust embedding. It belongs in Misc, and it needs to live
there for anything in the Misc session to use it: Shallow_Micro_Rust
already depends on Misc, so a Misc theory importing
Shallow_Micro_Rust.Num_Case_Expression would close a session dependency
cycle and make the Misc session unbuildable.

Its only in-tree consumer outside Misc, Shallow_Micro_Rust.Core_Syntax,
now reaches it as Misc.Num_Case_Expression.

Verified: `isabelle build -n -d . AutoCorrode` resolves the whole session
tree, and a full `isabelle build -d . Misc` checks every theory in the
session. (Document generation fails for want of a lualatex binary, both
here and at the pre-existing baseline -- unrelated to this change.)

Signed-off-by: Ike Mulder <ikemul@amazon.com>
Add a `simple_word_enum` command that generates the boilerplate for a type
whose inhabitants correspond one-to-one to a fixed list of distinct machine
words:

    simple_word_enum (32) my_enum =
        Answer = \<open>0x42\<close>
      | Best   = \<open>0x72\<close>

For a type T it generates T_variants (the word list) and
T_variants_distinct; the typedef of T over `set (map unat T_variants)` with
setup_lifting applied; one lifted constant per variant, with the transfer
rules collected in the named theorem bundles T_rep_defs (rep_eq) and T_defs
(abs_eq); T_all with T_all_concrete [code], T_all_distinct and T_all_total;
and finally everything setup_case_for_typedef provides, so that
`case _ of _ \<Rightarrow> _` works on the new type.

Values are ordinary terms, but note that hexadecimal literals need a
cartouche: the outer syntax lexes 0x42 as a Nat token followed by an
identifier, and Parse.term consumes a single token. Plain decimal numerals
need no cartouche.

Two supporting lemmas, simple_word_enum_distinct and
simple_word_enum_total, are stated once over an abstract
`type_definition Rep Abs (set (map unat ws))` so that the generated
T_all_distinct / T_all_total are single rule applications rather than metis
searches, as they were in the hand-written version this replaces.

Conversion functions between T and its representation word type are not
generated; the tests show how they are built on top.

To support this, setup_case_for_typedef's implementation is lifted out of
its `local` block in Case_for_Typedefs.thy into `structure
Case_For_Typedef`, exposing a thm-taking entry point plus a `verbose` flag,
so simple_word_enum can hand it freshly-proved theorems without a
theorem-name round-trip. The behaviour of the setup_case_for_typedef
command itself is unchanged.

Performance, measured on a 120-variant 64-bit enum (4.5s elapsed, 7.3s
cpu): the distinctness proof and the 120 lift_definition calls cost ~2s
each and dominate; everything else is under 0.3s, with all of define_all
at 0.03s. The distinctness proof deliberately uses the default simp set —
reducing `unat` of a numeral needs the word simp rules, no hand-picked
[simp only:] set was found that discharges it, and code_simp does not
terminate on a few dozen variants.

Verified: Misc.Simple_Word_Enums and Misc.Case_for_Typedefs check clean
(0 errors, 0 warnings) via ic2.

Signed-off-by: Ike Mulder <ikemul@amazon.com>
Two more plugins for the simple_word_enum plugin mechanism, both on by
default and suppressible with `plugins del:`.

generate_debug emits the generate_debug class instance, rendering each
variant as its own name. The case arms carry the bare names rather than
whole log_data lists, so the defining term holds one `str` application
instead of one per variant. No per-variant equations are generated:
generate_debug_T_def plus the case simproc reduces a concrete variant in
a fixed number of rewrites.

The instance has to go into the background theory, since classes are
global. The fact it declares there is not reliably reachable from the
target the declaration appeared in, and the namespace it lands in differs
between a fresh declaration and the retro-application the plugin
mechanism performs over already-declared enums at registration time. So
the background fact gets a concealed name and the exported theorem is
noted into the local target, as the other plugins do.

variant_equality registers a simproc deciding Ci = Cj on concrete
variants. Previously simp could not settle `Red = Blue`: T_all_distinct
states pairwise distinctness, but reaching a particular pair took a
T_index detour by hand. The simproc is built like the case_T one in
Case_for_Typedefs: it fires from the term net, guards on both sides being
concrete variants of the right type, and leaves anything else alone.
Identical variants go by refl; differing ones push T_index across the
assumed equation via a pre-instantiated arg_cong, reduce by
T_indices_concrete, and discharge the resulting numeral equation. Cost
per firing is independent of the variant count, and registration is 0ms
at every benchmarked size.

Also refreshes two comments that described superseded code: the
to_uN_pure_alt tactic notes (the by-cases proof and its quadratic
pitfalls are gone, the uniform proof is ~9ms at 64 variants) and the
benchmark prose (variants and variant_consts dominate now).
…imple_word_enum

Three changes to simple_word_enum, the last two coupled by a new
`urust: "Name"` clause.

Variants are now reachable type-qualified as `T.Ci`, not just bare, the
way datatype constructors are. This is a Local_Theory.const_alias under
`Binding.qualify false type_name` --- the same non-mandatory qualification
ctr_sugar gives datatype's constructors, so both spellings resolve to the
identical constant and neither shadows the other. typedef does the same
for Rep_T/Abs_T.

The declaration accepts an optional `urust: "Name"` clause after the type
name. Misc.Simple_Word_Enums does nothing with it: the name is recorded
on the enum_info handed to plugins, for a plugin to act on. The benchmark
command passes NONE, since a notation registration is a global effect that
would outlive its discarded local theory.

Two plugins in the new Shallow_Micro_Rust.Simple_Word_Enum_uRust consume
that name, independently selectable:

  urust_notation   -- each variant Ci as the uRust literal `Name::Ci`,
                      the registration StdLib_Ordering makes by hand for
                      the ordering datatype
  urust_conversion -- word_conversion's to/try_from functions lifted with
                      lift_fun1 into function_body form and registered as
                      calls, `Name::to_uN` / `Name::try_from`, mirroring
                      word_try_from_fun in Numeric_Types

Both are no-ops without a `urust:` clause. The lifted `_def` facts are
deliberately not tagged [micro_rust_simps]: whether a call unfolds to its
pure function during uRust reasoning is the caller's choice.

These live downstream rather than beside the command because
micro_rust_notation belongs to Shallow_Micro_Rust, which depends on Misc
--- registering a plugin from a downstream theory is what the plugin
mechanism is for (cf. bnf_lfp_size.ML against datatype). That dependency
also fixes the execution order: plugin.ML applies interpretations in
registration order (cons + fold_rev), and Misc is fully loaded before this
theory's ML runs, so word_conversion always precedes urust_conversion.

Only `plugins del: word_conversion` with urust_conversion left enabled can
break that, since the pure functions never get defined. It errors, naming
the missing constant and the contradictory pair, rather than silently doing
nothing; a test drives the failure through simple_word_enum_core under
Exn.capture and asserts on the message.

The uRust names are required to be plain identifiers: `Name::Ci` is a
::-path the frontend grammar already parses, so no bespoke production is
needed, but only if Name itself is an identifier. Rejecting at declaration
beats a registration whose use sites could never parse.
A case expression on a simple_word_enum was not code-exportable: case_T
reduces through match_T to find_index, which needs equality on T, so
export_code failed with "Type T not of sort equal". The new
equal_instance plugin supplies the instance as
`equal_T x y == Rep_T x = Rep_T y`, proved by Rep_T_inject. Constant
cost: 13-15ms from 4 through 64 variants.

A plugin rather than part of setup_case_for_typedef, because that command
runs on an existing typedef which may already be an equal instance, and a
class instance is global and unconditional. simple_word_enum creates the
typedef itself, so it knows the slot is free.

Separately, the generated constants had no code equations at all, so
exporting anything reaching T_variants or either conversion function
failed with "No code equations". They were defined via
Local_Theory.define, which attaches none; they now go through
Specification.definition, which attaches a default one. Being a default,
T_all_concrete's explicit [code] still supersedes it for T_all, as a test
asserts.

Tests evaluate equality and a case-based function, export the variants,
T_all, and the pure and uRust-lifted conversions to OCaml and SML, and
check that `plugins del: equal_instance` omits the instance.
@ike-mulder-aws
ike-mulder-aws merged commit ba31261 into awslabs:main Aug 6, 2026
14 checks passed
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.

1 participant