Skip to content

Commit aaec659

Browse files
committed
fix(ccl): execute branch and read-branch jump tables
A CCL program whose first instruction is branch failed in Neomacs with "Error in CCL program at 3th code" and exited 255. GNU Emacs runs the same program and prints the selected block. Reported in #435 with (branch r0 (write "A")), which ccl.el compiles to a vector whose first real word is opcode 0x0D (CCL_Branch). (write "A") alone already worked, because CCL_WriteConstString was implemented. The driver matched raw opcode numbers and treated every unimplemented code as an invalid program. CCL_Branch was one of those codes, so a valid jump table was rejected at vector index 2. GNU's driver (src/ccl.c) indexes the following words by the register when it is in range, and uses one extra slot otherwise. Each entry is a raw relative offset from the table head. CCL_ReadBranch reads one character and then uses that same table; EOF skips the table, and a suspended read resumes on the same word. CclCommand is now a repr(u8) enum of every GNU opcode. strum::FromRepr turns the 5-bit field into a variant with a safe const match, and the driver matches that enum with no wildcard. branch and read-branch share ccl_branch_target. Opcodes that are still unimplemented stay in one explicit arm and still signal "Error in CCL program". The new tests run GNU ccl-compile vectors: r0 selects "A", an out-of-range or negative register writes nothing, a later register selects "B", and read-branch covers both blocks, the extra slot, EOF, and suspend. Verified: cargo test -p neovm-core --offline --lib emacs_core::ccl:: (31 passed). The installed Neomacs binary was not rebuilt.
1 parent c8bd629 commit aaec659

3 files changed

Lines changed: 285 additions & 38 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//! CCL command field. GNU `src/ccl.c` assigns every 5-bit opcode.
2+
3+
/// One CCL command. Discriminants are the GNU opcode numbers (`code & 0x1F`).
4+
///
5+
/// The driver matches this enum with no wildcard, so adding a command is a
6+
/// compile error until that command has an execution arm. [`strum::FromRepr`]
7+
/// generates `from_repr`, a safe `const` match from those discriminants.
8+
#[repr(u8)]
9+
#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::FromRepr)]
10+
pub(super) enum CclCommand {
11+
SetRegister = 0x00,
12+
SetShortConst = 0x01,
13+
SetConst = 0x02,
14+
SetArray = 0x03,
15+
Jump = 0x04,
16+
JumpCond = 0x05,
17+
WriteRegisterJump = 0x06,
18+
WriteRegisterReadJump = 0x07,
19+
WriteConstJump = 0x08,
20+
WriteConstReadJump = 0x09,
21+
WriteStringJump = 0x0a,
22+
WriteArrayReadJump = 0x0b,
23+
ReadJump = 0x0c,
24+
Branch = 0x0d,
25+
ReadRegister = 0x0e,
26+
WriteExprConst = 0x0f,
27+
ReadBranch = 0x10,
28+
WriteRegister = 0x11,
29+
WriteExprRegister = 0x12,
30+
Call = 0x13,
31+
WriteConstString = 0x14,
32+
WriteArray = 0x15,
33+
End = 0x16,
34+
ExprSelfConst = 0x17,
35+
ExprSelfReg = 0x18,
36+
SetExprConst = 0x19,
37+
SetExprReg = 0x1a,
38+
JumpCondExprConst = 0x1b,
39+
JumpCondExprReg = 0x1c,
40+
ReadJumpCondExprConst = 0x1d,
41+
ReadJumpCondExprReg = 0x1e,
42+
Extension = 0x1f,
43+
}

‎crates/neovm-core/src/emacs_core/text/ccl/mod.rs‎

Lines changed: 111 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@
77
//! - `register-code-conversion-map` — stores named conversion maps and returns stable ids
88
//! - CCL-backed coding systems and `ccl-execute-on-string` share one bounded
99
//! bytecode machine, including resumable register/instruction state.
10+
//! - Each 5-bit opcode decodes to [`command::CclCommand`]. The driver matches
11+
//! that enum exhaustively; commands not yet executed still signal
12+
//! `Error in CCL program`.
1013
//! - `ccl-execute` — validates shape and designators while the remaining
1114
//! register-only instruction set is implemented incrementally.
1215
16+
mod command;
17+
18+
use self::command::CclCommand;
1319
use super::error::{EvalResult, Flow, signal};
1420
use super::value::*;
1521
use crate::emacs_core::SymId;
@@ -191,6 +197,31 @@ fn ccl_relative_instruction(instruction: usize, offset: i64) -> Option<usize> {
191197
usize::try_from(target).ok()
192198
}
193199

200+
/// GNU `CCL_Branch` (`src/ccl.c`). `table_head` is the first jump-table word.
201+
/// `length` table entries are followed by one out-of-range entry. Each entry
202+
/// is a raw relative offset from `table_head`, not a packed command.
203+
fn ccl_branch_target(
204+
words: &[i64],
205+
table_head: usize,
206+
length: i64,
207+
selector: i64,
208+
error_at: usize,
209+
) -> Result<usize, Flow> {
210+
let slot = if (0..length).contains(&selector) {
211+
selector
212+
} else {
213+
length
214+
};
215+
let slot = usize::try_from(slot).map_err(|_| invalid_ccl_program_at(error_at))?;
216+
let entry = table_head
217+
.checked_add(slot)
218+
.ok_or_else(|| invalid_ccl_program_at(error_at))?;
219+
let offset = *words
220+
.get(entry)
221+
.ok_or_else(|| invalid_ccl_program_at(error_at))?;
222+
ccl_relative_instruction(table_head, offset).ok_or_else(|| invalid_ccl_program_at(error_at))
223+
}
224+
194225
struct CclExecution {
195226
output: Vec<i64>,
196227
registers: [i64; 8],
@@ -238,7 +269,8 @@ fn execute_compiled_ccl_with_state(
238269
.ok()
239270
.filter(|register| *register < registers.len())
240271
.ok_or_else(|| invalid_ccl_program_at(this_instruction))?;
241-
let command = code & 0x1f;
272+
let command = CclCommand::from_repr((code & 0x1f) as u8)
273+
.ok_or_else(|| invalid_ccl_program_at(this_instruction))?;
242274

243275
let mut read_character = |destination: &mut i64| -> Option<bool> {
244276
if let Some(value) = input.get(source) {
@@ -254,39 +286,32 @@ fn execute_compiled_ccl_with_state(
254286
};
255287

256288
match command {
257-
// CCL_SetRegister
258-
0x00 => registers[register] = registers[other_register],
259-
// CCL_SetShortConst
260-
0x01 => registers[register] = field1,
261-
// CCL_SetConst
262-
0x02 => {
289+
CclCommand::SetRegister => registers[register] = registers[other_register],
290+
CclCommand::SetShortConst => registers[register] = field1,
291+
CclCommand::SetConst => {
263292
registers[register] = *words
264293
.get(instruction)
265294
.ok_or_else(|| invalid_ccl_program_at(this_instruction))?;
266295
instruction += 1;
267296
}
268-
// CCL_Jump
269-
0x04 => {
297+
CclCommand::Jump => {
270298
instruction = ccl_relative_instruction(instruction, field1)
271299
.ok_or_else(|| invalid_ccl_program_at(this_instruction))?;
272300
}
273-
// CCL_JumpCond
274-
0x05 if registers[register] == 0 => {
301+
CclCommand::JumpCond if registers[register] == 0 => {
275302
instruction = ccl_relative_instruction(instruction, field1)
276303
.ok_or_else(|| invalid_ccl_program_at(this_instruction))?;
277304
}
278-
0x05 => {}
279-
// CCL_WriteRegisterJump
280-
0x06 => {
305+
CclCommand::JumpCond => {}
306+
CclCommand::WriteRegisterJump => {
281307
output.push(registers[register]);
282308
instruction = ccl_relative_instruction(instruction, field1)
283309
.ok_or_else(|| invalid_ccl_program_at(this_instruction))?;
284310
}
285-
// CCL_WriteRegisterReadJump. The compiler stores a paired
286-
// CCL_ReadJump word after this fused instruction; GNU skips it
287-
// after a successful read, but resumes at that word when input is
288-
// exhausted in a non-final block.
289-
0x07 => {
311+
// The compiler stores a paired ReadJump word after this fused
312+
// instruction. GNU skips it after a successful read, but resumes
313+
// at that word when input is exhausted in a non-final block.
314+
CclCommand::WriteRegisterReadJump => {
290315
output.push(registers[register]);
291316
instruction = instruction
292317
.checked_add(1)
@@ -306,8 +331,7 @@ fn execute_compiled_ccl_with_state(
306331
}
307332
}
308333
}
309-
// CCL_WriteConstJump
310-
0x08 => {
334+
CclCommand::WriteConstJump => {
311335
output.push(
312336
*words
313337
.get(instruction)
@@ -316,8 +340,7 @@ fn execute_compiled_ccl_with_state(
316340
instruction = ccl_relative_instruction(instruction, field1)
317341
.ok_or_else(|| invalid_ccl_program_at(this_instruction))?;
318342
}
319-
// CCL_ReadJump
320-
0x0c => match read_character(&mut registers[register]) {
343+
CclCommand::ReadJump => match read_character(&mut registers[register]) {
321344
Some(true) => instruction = eof_instruction,
322345
Some(false) => {
323346
instruction = ccl_relative_instruction(instruction, field1)
@@ -331,9 +354,43 @@ fn execute_compiled_ccl_with_state(
331354
});
332355
}
333356
},
334-
// CCL_ReadRegister. Consecutive encoded operands read into one or
335-
// more registers; a zero field terminates the sequence.
336-
0x0e => {
357+
// `instruction` already points at the jump table. GNU indexes that
358+
// table by the register, or by `field1` when the register is
359+
// outside `0..field1`.
360+
CclCommand::Branch => {
361+
instruction = ccl_branch_target(
362+
&words,
363+
instruction,
364+
field1,
365+
registers[register],
366+
this_instruction,
367+
)?;
368+
}
369+
// GNU reads one character, then falls through into CCL_Branch.
370+
// EOF skips the table and runs the eof program. A suspended read
371+
// resumes on this same word.
372+
CclCommand::ReadBranch => match read_character(&mut registers[register]) {
373+
Some(true) => instruction = eof_instruction,
374+
Some(false) => {
375+
instruction = ccl_branch_target(
376+
&words,
377+
instruction,
378+
field1,
379+
registers[register],
380+
this_instruction,
381+
)?;
382+
}
383+
None => {
384+
return Ok(CclExecution {
385+
output,
386+
registers,
387+
instruction: this_instruction,
388+
});
389+
}
390+
},
391+
// Consecutive encoded operands read into one or more registers; a
392+
// zero field terminates the sequence.
393+
CclCommand::ReadRegister => {
337394
let mut read_field = field1;
338395
let mut read_register = register;
339396
loop {
@@ -365,8 +422,7 @@ fn execute_compiled_ccl_with_state(
365422
.ok_or_else(|| invalid_ccl_program_at(this_instruction))?;
366423
}
367424
}
368-
// CCL_WriteRegister
369-
0x11 => {
425+
CclCommand::WriteRegister => {
370426
let mut write_field = field1;
371427
let mut write_register = register;
372428
loop {
@@ -385,12 +441,11 @@ fn execute_compiled_ccl_with_state(
385441
.ok_or_else(|| invalid_ccl_program_at(this_instruction))?;
386442
}
387443
}
388-
// CCL_WriteConstString. A zero register field embeds one
389-
// character directly in FIELD1. A nonzero field stores an ASCII
390-
// string three octets per following word, most-significant octet
391-
// first (the representation emitted by GNU `ccl-embed-string`).
392-
0x14 if register == 0 => output.push(field1),
393-
0x14 => {
444+
// A zero register field embeds one character directly in FIELD1.
445+
// A nonzero field stores an ASCII string three octets per following
446+
// word, most-significant octet first (GNU `ccl-embed-string`).
447+
CclCommand::WriteConstString if register == 0 => output.push(field1),
448+
CclCommand::WriteConstString => {
394449
let length = usize::try_from(field1)
395450
.ok()
396451
.ok_or_else(|| invalid_ccl_program_at(this_instruction))?;
@@ -408,16 +463,34 @@ fn execute_compiled_ccl_with_state(
408463
}
409464
instruction = end;
410465
}
411-
// CCL_End. GNU leaves IC pointing at the End instruction so a
412-
// completed STATUS cannot accidentally resume beyond the vector.
413-
0x16 => {
466+
// GNU leaves IC pointing at the End instruction so a completed
467+
// STATUS cannot accidentally resume beyond the vector.
468+
CclCommand::End => {
414469
return Ok(CclExecution {
415470
output,
416471
registers,
417472
instruction: this_instruction,
418473
});
419474
}
420-
_ => return Err(invalid_ccl_program_at(this_instruction)),
475+
CclCommand::SetArray
476+
| CclCommand::WriteConstReadJump
477+
| CclCommand::WriteStringJump
478+
| CclCommand::WriteArrayReadJump
479+
| CclCommand::WriteExprConst
480+
| CclCommand::WriteExprRegister
481+
| CclCommand::Call
482+
| CclCommand::WriteArray
483+
| CclCommand::ExprSelfConst
484+
| CclCommand::ExprSelfReg
485+
| CclCommand::SetExprConst
486+
| CclCommand::SetExprReg
487+
| CclCommand::JumpCondExprConst
488+
| CclCommand::JumpCondExprReg
489+
| CclCommand::ReadJumpCondExprConst
490+
| CclCommand::ReadJumpCondExprReg
491+
| CclCommand::Extension => {
492+
return Err(invalid_ccl_program_at(this_instruction));
493+
}
421494
}
422495
}
423496

0 commit comments

Comments
 (0)