-
Notifications
You must be signed in to change notification settings - Fork 54
feat(Computability): add Unlimited Register Machines #299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
388bd04 to
6f272d5
Compare
Introduce Unlimited Register Machines (URMs), a model of computation with instructions, programs, and registers. URMs are a standard topic in computability theory, introduced by Shepherdson and Sturgis (1963) and popularized by Cutland's textbook. Core definitions: - Instr: The four instruction types (Z, S, T, J) - Program: Lists of instructions - State: Register contents as ℕ → ℕ - Config: Machine configuration (program counter + state) Execution semantics: - Step/Steps: Single and multi-step execution relations - Halts, Diverges, HaltsWithResult: Halting predicates - eval: Evaluation as a partial function - ProgramEquiv: Program equivalence with Setoid instance (≈ notation) - Standard notation: p ↓ inputs, p ↑ inputs, p ↓ inputs ≫ result Computability: - Computes: A program computes a given partial function - Computable: Existence of a program that computes a function - Basic computable functions: successor, projection, identity Additional modules: - StraightLine: Jump-free programs that always halt - StandardForm: Programs with bounded jump targets, with normalization
6f272d5 to
09b7076
Compare
Cslib/Computability/Urm/State.lean
Outdated
|
|
||
| Uses the functional representation `ℕ → ℕ` for efficiency with rewrites, | ||
| following the advice from the `grind` tactic documentation. -/ | ||
| abbrev State := ℕ → ℕ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Representing states as functions from Nat to Nat is convenient here, but I worry that it would lead to problems down the road. The other choice I have in mind is to let the registers of a UTM be indexed by an arbitrary (or perhaps [Fintype]) type, and then have State be defined State (m : UTM) := u.RegisterIndex -> Nat as machine-dependent type. You would have to include this extra variable. But the benefit would be that if you want to define a composition of the functionalities of different UTMs (like I do for TMs in #269), you can do so by taking a direct product of the individual register indices, which would hopefully make functions on that register type easier to define. It would also have the benefit, if the register type were finite, of having the register state be printable data.
Perhaps this change is not necessary since the textbook defines it this way, but it's something to think about.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this takes too long to figure out, I think it's fine to proceed with the current version. In general, we might wanna have a general 'RegisterMachine' concept and then apply it here, but it might be good to accumulate some experience first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love the idea of a general register machine concept! What I might suggest is implementing another register machine model, say RASP machines (but that's just one possibility!).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One challenge with the use of an arbitrary index type for the register indices, or [Fintype], would be to ensure the coherence of programs. Using natural numbers, one has the luxury of knowing that any program is coherent in the sense that any register index appearing in it refers to a register that actually exists. Programs and machines (or states) are sort of independent. But perhaps I'm overthinking this; making the definition of machine states and programs share an index type could work (and might have some nice theoretical benefits such as enabling a notion of product of machines).
chenson2018
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty good to me, and thank you for documenting things well. Here are some overarching thoughts:
- I think the duplication of API between
BoolandPropthat happens in several places is unnecessary - I made a lot of comments about how you can use
grind. These are not all mandatory (I have a personal inclination to use it a lot) but I think worth it overall. Definitions/theorems I passed directly togrindoften should have annotations for these. - I wasn't completely comfortable with the use of choice in some type signatures, but I need to think more carefully about this
Cslib/Computability/Urm/Instr.lean
Outdated
| - `J m n q`: If registers m and n have equal contents, jump to instruction q; | ||
| otherwise proceed to the next instruction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the jump instruction's semantics is changed to a relative jump (namely, the jump target is specified via an offset relative to the jump instruction's own position, rather than an absolute position), would shift_jumps and all related stuff still be needed? Of course this would require either q be an integer or the jump instruction be split into a jump_forward and a jump_backward instructions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting idea! I think relative offsets would be fine (and shouldn't affect the class of computable functions). This change would indeed obviate the need for shift_jumps and similar bureaucratic details, but some details would just get shifted to other places. On balance, though, the net gain of a relative approach might be worth it. I'm not sure. I'd be happy to explore this idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about this some more, and I suggest exploring this in a follow-on PR. A syntactic conversion between programs is straightforward and an equivalence proof should also be pretty easy. In my opinion, this kind of exercise belongs in CSLib. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's up to you, for you will have to do the proofs. PC-relative jump is in fact an old idea. Every modern instruction set architecture has it, precisely for the purpose of supporting position-independent code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A related idea is to add a base register and/or literal to each instruction, so that each address is relative to that base, rather than an absolute address. Here you can probably get away with using only Nat for address.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's some spectrum of tradeoff here from staying minimal and abstract to mirroring a real instruction set. I'm fine if this stays simple at first, especially if that will make your future plans for connecting to SKI simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Come to think of it: you can have a special register (say, registter 0) which contains a "base PC". The real target of a jump instruction is the target + the value of the base PC. This will also give you position-independent code at the expense of changing the base PC value when switching between different blocks of code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Chris. Adding more "realistic" instruction features is easy. The hard part is to show that the fancier instructions can be implemented by a more primitive formalism like SKI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first instinct here is to investigate the relative jump idea. That ought to simplify the proofs of closure of URM-computable functions under composition, primitive recursion, and minimization. And it should help with the SKI equivalence proof, too.
- Define IsJump, JumpsBoundedBy, IsStraightLine directly as Prop - Remove redundant Bool versions (isJump, jumps_bounded_by, etc.) - Rename theorems: not_IsJump → nonJump
Co-authored-by: Chris Henson <46805207+chenson2018@users.noreply.github.com>
Co-authored-by: Chris Henson <46805207+chenson2018@users.noreply.github.com>
Co-authored-by: Chris Henson <46805207+chenson2018@users.noreply.github.com>
Swap argument order so Program comes before offset, enabling nicer dot notation like `p.shift_jumps offset`. Also fix redundant `rename_i` in StandardForm.lean.
Add `evalConfig : Program → List ℕ → Part Config` as the primary definition, with `eval` as a wrapper via `Part.map`. This removes `Classical.choose` from the type signatures of `toHaltsWithResult` and `evalConfig_to_standard_form_state`.
Introduce `straightLine_finalConfig` to wrap the choice, mirroring the pattern used for `evalConfig`. Updates `straightLine_finalConfig_spec` to use the wrapper instead of exposing `Classical.choose` in its type.
I've refactored the theorems that use choice to use the |
fmontesi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nice work! I've added some comments and questions myself.
|
|
||
| /-- The step relation is deterministic: each configuration has at most one successor. -/ | ||
| theorem deterministic {c c' c'' : Config} (h1 : Step p c c') (h2 : Step p c c'') : c' = c'' := by | ||
| cases h1 <;> cases h2 <;> grind |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chenson2018, in LTS we have the class Deterministic to cover exactly this. Would it make sense to introduce a similar class for ReductionSystem and use it here?
| Relation.ReflTransGen.trans h1 h2 | ||
|
|
||
| /-- One step implies multi-step. -/ | ||
| theorem single {c c' : Config} (h : Step p c c') : Steps p c c' := |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As Chris mentioned, some of these things can come for free from ReductionSystem.
|
|
||
| /-- A program halts on given inputs if there exists a halted configuration reachable from | ||
| the initial configuration. -/ | ||
| def Halts (inputs : List ℕ) : Prop := |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chenson2018: if we don't/cannot generalise this to ReductionSystem now, I'd like a concurrent issue to be opened so that we remember.
In general, concepts like divergence and termination should probably be unified under general definitions for ReductionSystem and LTS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like that idea. I'll investigate using Deterministic. I tried building a bit on top of LTSs but I couldn't think of a good match with URM semantics, but it looks like I need to investigate LTSs some more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think ReuctionSystem is pretty straightforward in replacing Step/Steps. I'd say give it a try and if we hit any significant difficulties we open an issue as Fabrizio has suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll give that a try; I'd really like to align with existing infrastructure. Stay tuned!
Per textbook convention: State is full machine state (pc + registers), Regs is just register contents.
Summary
This PR introduces Unlimited Register Machines (URMs), a model of computation with instructions, programs, and registers. URMs are a standard topic in computability theory, introduced by Shepherdson and Sturgis and popularized by Cutland's textbook.
What's included
Core definitions (
Defs.lean):Instr: The four instruction types (Z, S, T, J)Program: Lists of instructions, withshift_jumps,shift_registers,max_registerState: Register contents asℕ → ℕConfig: Machine configuration (program counter + state)Basic lemmas (
Basic.lean):State.write_read_self,State.write_read_of_ne: State read/write lemmasConfig.is_halted_iff,Config.ext: Configuration lemmasisJump,IsJump: Jump instruction predicatesjumps_bounded_by,JumpsBoundedBy: Bounded jump targetscap_jump: Cap jump targets to a given lengthExecution semantics (
Execution.lean):Step: Single-step execution relationSteps: Multi-step execution (reflexive-transitive closure)Halts,Diverges,HaltsWithResult: Halting predicateseval: Evaluation as a partial functionProgramEquiv: Program equivalence withSetoidinstance (≈notation)p ↓ inputs,p ↑ inputs,p ↓ inputs ≫ resultStep.deterministic,Steps.eq_of_haltsComputability (
Computable.lean):Computes: A program computes a given partial functionComputable: A partial function is URM-computable (∃ program that computes it)Straight-line programs (
StraightLine.lean):IsStraightLine: Programs without jumpsstraight_line_halts: Straight-line programs always haltStandard form (
StandardForm.lean):IsStandardForm: Programs with bounded jump targetsto_standard_form: Normalization functionto_standard_form_equiv: Behavioral equivalence (p.to_standard_form ≈ p)to_standard_form_idempotent: Idempotence of normalizationDesign decisions
State := ℕ → ℕworks well withgrindandsimpStepas an inductive relation rather than a function, following the style of Mathlib's Turing machine formalizationFeedback welcome
I'm happy to adjust naming conventions, reorganize the module structure, or refine the API based on reviewer preferences. This is my first contribution to CSLib, so guidance on project conventions is appreciated.