~james/remy

feat(branches): compile conditionals to validated forward jumps

commit
b58768f9b4b75a882c2394ff6c2135d32062939d browse tree
parent
edf26390ae3eb8281b7adc9bb0e40f56de1941a8
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-11-01 03:08 UTC

6 files changed, +227 −6

README.md modified

@@ -31,6 +31,6 @@ Each `let` pushes a binding onto the evaluation stack. [Compilation](src/core/Co
3131
3232 Arithmetic wraps modulo `2^64` and shifts mask their count to six bits
3333
34-Both branches of a nonconstant conditional run before selection which preserves the result because expressions have no side effects. The HolyC runner masks each pixel result to four bits for the TempleOS palette
34+Nonconstant conditionals use forward `jz` and `jmp` instructions so only the selected arm runs. The validator checks both arms for matching stack heights and protected input registers. The HolyC runner masks each result to four bits for the TempleOS palette
3535
3636 The execution theorem is relative to the instruction block model. Actual x86 execution + the HolyC calling bridge and the extracted runtime and the OS are outside its scope. Tests compare emitted code against an independent evaluator on Linux and against HOL results inside the recorded TempleOS guest

ROOT modified

@@ -15,6 +15,7 @@ session Remy in src = Remybase +
1515 sessions "HOL-Library"
1616 theories
1717 Allocation
18+ Branches
1819 Frontend
1920 Examples
2021 Export

src/Branches.thy added

@@ -0,0 +1,169 @@
1+theory Branches
2+ imports Allocation
3+begin
4+
5+datatype flow = Basic "opcode list" | Sequence flow flow | Choice flow flow
6+
7+fun flowrun :: "flow \<Rightarrow> machine \<Rightarrow> machine option" where
8+ "flowrun (Basic code) s = run code s"
9+| "flowrun (Sequence a b) s = Option.bind (flowrun a s) (flowrun b)"
10+| "flowrun (Choice a b) s = flowrun (if registers s Rax = 0 then b else a) s"
11+
12+fun chain :: "flow list \<Rightarrow> flow" where
13+ "chain [] = Basic []"
14+| "chain (a # code) = Sequence a (chain code)"
15+
16+fun branch_allocate :: "bool \<Rightarrow> expression \<Rightarrow> nat list \<Rightarrow> flow" where
17+ "branch_allocate free (Literal v) slots = Basic [Immediate v]"
18+| "branch_allocate free (Input i) slots = Basic [Move Rax (input_register i)]"
19+| "branch_allocate free (Variable i) slots = Basic [Peek (slots ! i)]"
20+| "branch_allocate free (Binary op a b) slots = (if free then
21+ chain [branch_allocate True a slots, Basic [Move Rbx Rax], branch_allocate False b slots,
22+ Basic [Move Rcx Rbx, Arithmetic op]]
23+ else chain [branch_allocate False a slots, Basic [Push Rax], branch_allocate False b (map Suc slots),
24+ Basic [Pop Rcx, Arithmetic op]])"
25+| "branch_allocate free (Cond c a b) slots = Sequence (branch_allocate free c slots)
26+ (Choice (branch_allocate free a slots) (branch_allocate free b slots))"
27+| "branch_allocate free (Let a b) slots = chain [branch_allocate free a slots, Basic [Push Rax],
28+ branch_allocate free b (0 # map Suc slots), Basic [Pop Rcx]]"
29+
30+theorem branch_allocate_correct:
31+ assumes "scoped (length env) e" "represents slots (stack s) env"
32+ shows "\<exists>q. flowrun (branch_allocate free e slots) s = Some q \<and> allocated_result free e env s q"
33+ using assms
34+proof (induction e arbitrary: free slots env s)
35+ case (Literal v)
36+ then show ?case by (auto simp: allocated_result_def chain.simps)
37+next
38+ case (Input i)
39+ then show ?case by (auto simp: allocated_result_def inputs_def fun_eq_iff)
40+next
41+ case (Variable i)
42+ then show ?case by (auto simp: allocated_result_def represents_def)
43+next
44+ case (Binary op a b)
45+ obtain p where a: "flowrun (branch_allocate free a slots) s = Some p"
46+ and p: "allocated_result free a env s p"
47+ using Binary.IH(1)[of env slots s free] Binary.prems by auto
48+ show ?case
49+ proof (cases free)
50+ case True
51+ let ?p = "p\<lparr>registers := (registers p)(Rbx := registers p Rax)\<rparr>"
52+ obtain q where b: "flowrun (branch_allocate False b slots) ?p = Some q"
53+ and q: "allocated_result False b env ?p q"
54+ using Binary.IH(2)[of env slots ?p False] Binary.prems p
55+ by (auto simp: allocated_result_def chain.simps)
56+ show ?thesis using a b p q True
57+ by (auto simp: allocated_result_def run_append flowrun.simps chain.simps; auto simp: inputs_def fun_eq_iff)
58+ next
59+ case False
60+ let ?p = "p\<lparr>stack := registers p Rax # stack p\<rparr>"
61+ obtain q where b: "flowrun (branch_allocate False b (map Suc slots)) ?p = Some q"
62+ and q: "allocated_result False b env ?p q"
63+ using Binary.IH(2)[of env "map Suc slots" ?p False] Binary.prems p
64+ represents_push[OF Binary.prems(2), of "registers p Rax"]
65+ by (auto simp: allocated_result_def chain.simps)
66+ show ?thesis using a b p q False
67+ by (auto simp: allocated_result_def run_append flowrun.simps chain.simps; auto simp: inputs_def fun_eq_iff)
68+ qed
69+next
70+ case (Cond c a b)
71+ obtain p where c: "flowrun (branch_allocate free c slots) s = Some p"
72+ and p: "allocated_result free c env s p"
73+ using Cond.IH(1)[of env slots s free] Cond.prems by auto
74+ show ?case
75+ using Cond.IH(2)[of env slots p free] Cond.IH(3)[of env slots p free]
76+ Cond.prems c p
77+ by (auto simp: allocated_result_def split: if_splits)
78+next
79+ case (Let a b)
80+ obtain p where a: "flowrun (branch_allocate free a slots) s = Some p"
81+ and p: "allocated_result free a env s p"
82+ using Let.IH(1)[of env slots s free] Let.prems by auto
83+ let ?p = "p\<lparr>stack := registers p Rax # stack p\<rparr>"
84+ obtain q where b: "flowrun (branch_allocate free b (0 # map Suc slots)) ?p = Some q"
85+ and q: "allocated_result free b (registers p Rax # env) ?p q"
86+ using Let.IH(2)[of "registers p Rax # env" "0 # map Suc slots" ?p free]
87+ Let.prems p represents_bind[OF Let.prems(2), of "registers p Rax"]
88+ by (auto simp: allocated_result_def chain.simps)
89+ show ?case using a b p q
90+ by (auto simp: allocated_result_def run_append flowrun.simps chain.simps; auto simp: inputs_def fun_eq_iff)
91+qed
92+
93+definition branching :: "expression \<Rightarrow> flow" where
94+ "branching e = branch_allocate True (optimise e) []"
95+
96+theorem branching_return:
97+ "scoped 0 e \<Longrightarrow> \<exists>q. flowrun (branching e) s = Some q \<and>
98+ registers q Rax = evaluate e [] (inputs s) \<and> stack q = stack s \<and> inputs q = inputs s"
99+ using branch_allocate_correct[where env="[]" and slots="[]" and free=True and e="optimise e"]
100+ optimise_scoped
101+ by (auto simp: branching_def allocated_result_def represents_def optimise_correct)
102+
103+fun flowaudit :: "flow \<Rightarrow> nat \<Rightarrow> nat option" where
104+ "flowaudit (Basic code) n = audit code n"
105+| "flowaudit (Sequence a b) n = Option.bind (flowaudit a n) (flowaudit b)"
106+| "flowaudit (Choice a b) n = (case (flowaudit a n, flowaudit b n) of
107+ (Some x,Some y) \<Rightarrow> if x = y then Some x else None | _ \<Rightarrow> None)"
108+
109+theorem flowaudit_sound:
110+ "flowaudit code (length (stack s)) = Some n \<Longrightarrow>
111+ \<exists>q. flowrun code s = Some q \<and> length (stack q) = n \<and> inputs q = inputs s"
112+proof (induction code arbitrary: s n)
113+ case (Basic code)
114+ then show ?case using audit_sound audit_inputs by fastforce
115+next
116+ case (Sequence a b)
117+ then show ?case by (auto split: option.splits bind_splits; fastforce)
118+next
119+ case (Choice a b)
120+ then show ?case by (auto split: option.splits if_splits; fastforce)
121+qed
122+
123+fun flowbytes :: "flow \<Rightarrow> nat list" where
124+ "flowbytes (Basic code) = concat (map encode code)"
125+| "flowbytes (Sequence a b) = flowbytes a @ flowbytes b"
126+| "flowbytes (Choice a b) = [72,133,192,15,132] @ octets 4 (length (flowbytes a) + 5) @
127+ flowbytes a @ [233] @ octets 4 (length (flowbytes b)) @ flowbytes b"
128+
129+fun flowvalid :: "flow \<Rightarrow> bool" where
130+ "flowvalid (Basic code) = list_all encodable code"
131+| "flowvalid (Sequence a b) = (flowvalid a \<and> flowvalid b)"
132+| "flowvalid (Choice a b) = (flowvalid a \<and> flowvalid b \<and>
133+ length (flowbytes a) + 5 < 2 ^ 31 \<and> length (flowbytes b) < 2 ^ 31)"
134+
135+lemma flowbytes_bytes: "set (flowbytes code) \<subseteq> {..<256}"
136+ by (induction code) (auto dest: subsetD[OF encode_bytes] subsetD[OF octets_bytes])
137+
138+function unpack :: "nat \<Rightarrow> nat list \<Rightarrow> flow option" where
139+ "unpack 0 bytes = (if bytes = [] then Some (Basic []) else None)"
140+| "unpack (Suc fuel) bytes = (if bytes = [] then Some (Basic []) else
141+ if take 5 bytes = [72,133,192,15,132] then
142+ let offset = integer (take 4 (drop 5 bytes)); pos = 9 + (offset - 5);
143+ count = integer (take 4 (drop (Suc pos) bytes)) in
144+ if 9 \<le> length bytes \<and> 5 \<le> offset \<and> pos + 5 + count \<le> length bytes \<and>
145+ bytes ! pos = 233 then
146+ Option.bind (unpack fuel (take (offset - 5) (drop 9 bytes))) (\<lambda>a.
147+ Option.bind (unpack fuel (take count (drop (pos + 5) bytes))) (\<lambda>b.
148+ map_option (Sequence (Choice a b)) (unpack fuel (drop (pos + 5 + count) bytes))))
149+ else None
150+ else Option.bind (decode bytes) (\<lambda>(op,rest).
151+ map_option (Sequence (Basic [op])) (unpack fuel rest)))"
152+ by pat_completeness auto
153+termination by (relation "measure (\<lambda>(fuel,bytes). fuel)") auto
154+
155+declare unpack.simps [simp del]
156+
157+definition inspect :: "nat list \<Rightarrow> flow option" where
158+ "inspect bytes = (if bytes = [] \<or> last bytes \<noteq> 195 \<or> \<not> list_all (\<lambda>b. b < 256) bytes then None else
159+ Option.bind (unpack (length bytes) (butlast bytes)) (\<lambda>code.
160+ if flowvalid code \<and> flowbytes code = butlast bytes \<and> flowaudit code 0 = Some 0
161+ then Some code else None))"
162+
163+theorem inspected_sound:
164+ "inspect bytes = Some code \<Longrightarrow> \<exists>q. flowrun code (s\<lparr>stack := []\<rparr>) = Some q \<and>
165+ stack q = [] \<and> inputs q = inputs s"
166+ using flowaudit_sound[where s="s\<lparr>stack := []\<rparr>" and n=0]
167+ by (auto simp: inspect_def split: if_splits option.splits bind_splits)
168+
169+end

src/Frontend.thy modified

@@ -1,5 +1,5 @@
11 theory Frontend
2- imports Allocation
2+ imports Branches
33 begin
44
55 fun finish :: "string \<Rightarrow> string list \<Rightarrow> string list" where
@@ -206,14 +206,33 @@ fun listing :: "nat \<Rightarrow> (nat list \<times> opcode) list \<Rightarrow>
206206 '' ['' @ concat (map (\<lambda>b. decimal b @ '' '') bytes) @ '']'' @ newline @
207207 listing (offset + length bytes) code"
208208
209+fun linear_listing :: "nat \<Rightarrow> opcode list \<Rightarrow> string" where
210+ "linear_listing offset [] = []"
211+| "linear_listing offset (op # code) = decimal offset @ '' '' @ mnemonic op @ newline @
212+ linear_listing (offset + length (encode op)) code"
213+
214+fun flowlisting :: "nat \<Rightarrow> flow \<Rightarrow> string" where
215+ "flowlisting offset (Basic code) = linear_listing offset code"
216+| "flowlisting offset (Sequence a b) = flowlisting offset a @
217+ flowlisting (offset + length (flowbytes a)) b"
218+| "flowlisting offset (Choice a b) = decimal offset @ '' test rax rax'' @ newline @
219+ decimal (offset + 3) @ '' jz '' @ decimal (offset + 14 + length (flowbytes a)) @ newline @
220+ flowlisting (offset + 9) a @ decimal (offset + 9 + length (flowbytes a)) @ '' jmp '' @
221+ decimal (offset + length (flowbytes (Choice a b))) @ newline @
222+ flowlisting (offset + 14 + length (flowbytes a)) b"
223+
209224 definition disassemble :: "string \<Rightarrow> bool \<times> string" where
210225 "disassemble bytes = (case blocks (map nat_of_char bytes) of
211- None \<Rightarrow> (False,''invalid native block stream'' @ newline)
226+ None \<Rightarrow> (case inspect (map nat_of_char bytes) of
227+ None \<Rightarrow> (False,''invalid native block stream'' @ newline)
228+ | Some code \<Rightarrow> (True,flowlisting 0 code @ decimal (length (flowbytes code)) @ '' ret'' @ newline))
212229 | Some code \<Rightarrow> (True,listing 0 code))"
213230
214231 definition validate :: "string \<Rightarrow> bool \<times> string" where
215232 "validate bytes = (case blocks (map nat_of_char bytes) of
216- None \<Rightarrow> (False,''invalid native block stream'' @ newline)
233+ None \<Rightarrow> (case inspect (map nat_of_char bytes) of
234+ None \<Rightarrow> (False,''invalid native block stream'' @ newline)
235+ | Some code \<Rightarrow> (True,''native branch, stack and register effects verified'' @ newline))
217236 | Some code \<Rightarrow> if audit (map snd code) 0 = Some 0
218237 then (True,''native stack and register effects verified'' @ newline)
219238 else (False,''invalid native stack or register effects'' @ newline))"
@@ -272,8 +291,8 @@ definition source_error_with :: "limits \<Rightarrow> string \<Rightarrow> strin
272291
273292 definition artifact_with :: "limits \<Rightarrow> string \<Rightarrow> (bool \<times> string) \<times> string" where
274293 "artifact_with lim s = (case prepare_with lim s of None \<Rightarrow> ((False,[]), source_error_with lim s)
275- | Some e \<Rightarrow> let opt = optimise e; code = allocated e; bytes = allocated_bytes e in
276- if audit code 0 = Some 0 \<and> list_all encodable code then
294+ | Some e \<Rightarrow> let opt = optimise e; code = branching e; bytes = flowbytes code @ [195] in
295+ if flowaudit code 0 = Some 0 \<and> flowvalid code then
277296 ((True, map char_of_nat bytes), decimal (nodes e) @ '' nodes -> '' @ decimal (nodes opt) @
278297 '' nodes, '' @ decimal (length bytes) @ '' bytes, '' @ decimal (demand opt) @ '' stack words'' @ newline)
279298 else ((False,[]),''allocated code exceeds native limits'' @ newline))"

tests/cli.py modified

@@ -58,6 +58,21 @@ def main():
5858 invoke(source,binary)
5959 listing = invoke('disassemble',binary)
6060 assert 'push rax' in listing and 'pop rcx' in listing
61+ source.write_text('(if x (+ y 7) (- t 9))')
62+ invoke(source,binary)
63+ listing = invoke('disassemble',binary)
64+ assert 'jz ' in listing and 'jmp ' in listing and 'conditional ' not in listing
65+ invoke('check',binary)
66+ original = binary.read_bytes()
67+ branch = original.index(bytes.fromhex('48 85 c0 0f 84'))
68+ for offset in (0,4,0x7fffffff,0xffffffff):
69+ damaged = bytearray(original)
70+ damaged[branch+5:branch+9] = offset.to_bytes(4,'little')
71+ binary.write_bytes(damaged)
72+ invoke('check',binary,ok=False)
73+ for cut in range(branch+1,branch+9):
74+ binary.write_bytes(original[:cut]+bytes([195]))
75+ invoke('check',binary,ok=False)
6176 for malformed in [b'',bytes([195,0]),bytes([72,199,192,0]),bytes([80])]:
6277 binary.write_bytes(malformed)
6378 assert 'invalid native' in invoke('disassemble',binary,ok=False)

tests/native.py modified

@@ -136,6 +136,23 @@ def main():
136136 comparisons = 0
137137 with tempfile.TemporaryDirectory() as directory:
138138 path, binary = Path(directory) / "source", Path(directory) / "binary"
139+ path.write_text('(if x (+ y 7) (- t 9))')
140+ subprocess.run([ROOT / '.build/remy',path,binary],check=True,capture_output=True)
141+ conditional = binary.read_bytes()
142+ start = conditional.index(bytes.fromhex('48 85 c0 0f 84'))
143+ otherwise = start + 9 + int.from_bytes(conditional[start+5:start+9],'little')
144+ damaged = bytearray(conditional)
145+ damaged[otherwise:otherwise+2] = bytes.fromhex('0f 0b')
146+ assert execute(bytes(damaged),[(1,3,2)]) == [10]
147+ try:
148+ execute(bytes(damaged),[(0,3,2)])
149+ except RuntimeError as error:
150+ assert 'status -4' in str(error), error
151+ else:
152+ raise AssertionError('selected poisoned branch did not execute')
153+ damaged = bytearray(conditional)
154+ damaged[start+9:start+11] = bytes.fromhex('0f 0b')
155+ assert execute(bytes(damaged),[(0,3,2)]) == [MASK-6]
139156 for case in cases:
140157 path.write_text(source(case))
141158 result = subprocess.run([ROOT / ".build/remy", path, binary], capture_output=True, text=True, timeout=30)