Guidance for AI agents working on Charon.
-
I like to work together in the same workspace. You make edits as I request them, and I amend the code you produced until it meets my standards.
-
This does mean the code may change under you while you're editing something else. That's how I work, I do my best to not break what you're working on. Do not revert my changes even if you preferred your way. If you think I missed why you did things a certain way, tell me! I can often be shortsighted.
-
I will carefully review all your changes. I am in charge of commits and PRs etc.
-
Treat the git state as my domain: I will stage the bits I have reviewed. I may sometimes ask you to make commits but otherwise don't touch the git state.
-
Always try to keep git diffs small to make my job easier. I particularly dislike small helpers used only once; I prefer a well-placed comment to make the code easy to follow.
-
Charon is written with the following values in mind: correctness, robustness, maintainability, and solving people's problems (also, finding joy in clean code and abstractions).
-
The codebase is built around carefully-crafted invariants, e.g. how we go from rustc to hax to charon for
GenericArgs, or how mono mode is transparent most of the time. -
Read the surrounding code before editing. Most problems in this repository already have a nearby pattern in another translation pass, AST utility, or normalization pass.
-
We are the owners of this codebase, we can make deep changes if that's the best way to solve a problem.
-
Exercise judgment: sometimes the right fix is downstream (e.g. in aeneas) or upstream (e.g. in rustc itself); I don't shy from fixing the bugs where they should be fixed (though well-placed workarounds are often fine).
-
Correctness comes first. We should be very careful when modifying what we got from rustc. Even tricky cases should be handled correctly; if in doubt, double-check and add tests.
-
Robustness comes second. Charon should be able to emit an llbc file even in the face of many errors. Use panics appropriately, when they indicate a broken invariant within Charon. Use errors when the invariant is less clear and/or there's high risk of it being broken in practice.
-
Maintainability comes third. Prefer small, principled changes over local cleverness. A change that introduces a ton of code at once is suspicious; there's often a cleaner way. I'm the sole maintainer so any unneeded complexity is a cost I'll have to bear in the future.
-
If in doubt, ask, and exercise judgment as a good OSS maintainer would.
-
If you can't find a true solution, stop and ask rather than compromising with a workaround. This is particularly important for global invariants like having the right generics/trait clauses.
-
Once you're done with a change, review it from the pov of an architect: does it use the right abstractions for the job, does it make use or/enforce the important invariants, will the code be easy to evolve.
-
I will likely work in the same repo in parallel to start cleaning up your changes; I try not to interfere too much but don't be surprised.
-
Don't make or amend commits unless explicitly asked, I like to handcraft commits.
- Avoid constructing generic arguments manually to avoid getting trait references wrong. Prefer obtaining generics from an existing call, type, translated item, or Hax/rustc API.
- Binder handling must be explicit and correct. When visiting under binders, you can use
visitor wrappers (
VisitWithBinderDepthandVisitWithBinderStack) to help. - Be careful about monomorphized mode: it can break some assumptions such as the fact that a given item exists only once. If an approach relies on polymorphic item paths, function names, or non-monomorphized generic structure, either make it work in mono deliberately or gate the pass off in mono mode.
- Prefer rustc and Hax APIs for item discovery and method resolution. Use
def_path_def_ids/path resolution for named standard items, and HaxItemRefs when the result should flow through normal Charon translation. - Be careful with
ctx.translated: some bodies stored there may not have gone through later body cleanup passes yet. If a pass reads from translated bodies while transformations are fused, consider whether it is seeing pre-pass or post-pass state. - If a pass needs a declaration id, prefer an initializer (
Transform::new(ctx)) that discovers it once and stores it in the transform. Put prerequisites inshould_run, including option gates and whether required declarations were found. - Keep algorithmic complexity under control: generally avoid iterating over the whole crate or a whole body too many times.
- Every fix should have a reproducer whenever feasible. The test suite is quite flexible, use it.
Note that it supports negative tests, with
//@ known-failure. - Start by adding relevant tests before changing behavior. Make a commit named "Add tests" that includes the generated output, so that we can observe changes to it.
- Prefer precise flags over using
--presetas this makes more obvious which pass is at fault. - Changes to the generated output are to be double-checked: they can often indicate a bug. Well-motivated changes are however totally fine.
- Always do a full
make test && make clippyat the end of a change.
Charon is a hybrid codebase. A typical feature is mostly on the Rust side. However when the AST
changes, this must be propagated. Use make generate-ml to regenerate the generated OCaml files.
make test at the root of the repo tests both the OCaml and Rust sides.
Any change to the AST must come with a version bump to make the deserializers emit nice errors. Just
bump the patch version in Cargo.toml then run make test at the root to propagate the changes.