Functional programming 用像OCaml这样的函数式语言实现直接线程解释器

Functional programming 用像OCaml这样的函数式语言实现直接线程解释器,functional-programming,ocaml,interpreter,Functional Programming,Ocaml,Interpreter,在C/C++中,可以使用函数指针数组实现直接线程解释器。数组表示您的程序—一个操作数组。每个操作函数必须以对数组中下一个函数的调用结束,如: void op_plus(size_t pc, uint8_t* data) { *data += 1; BytecodeArray[pc+1](pc+1, data); //call the next operation in the array } let op_plus pc data = Printf.printf "pc: %d, da

在C/C++中,可以使用函数指针数组实现直接线程解释器。数组表示您的程序—一个操作数组。每个操作函数必须以对数组中下一个函数的调用结束,如:

void op_plus(size_t pc, uint8_t* data) {
  *data += 1;
  BytecodeArray[pc+1](pc+1, data); //call the next operation in the array
}
let op_plus pc data = Printf.printf "pc: %d, data_i: %d \n" pc data;
                        let f = (op_array.(pc+1)) in         
                        f (pc+1) (data+1) ;;
let rec op_plus pc data = ...
and op_array = [| ... |]
字节码数组是一个函数指针数组。如果我们有一个包含这些运算的数组,那么数组的长度将决定我们增加数据内容的频率。(当然,您需要添加某种终止操作作为数组中的最后一个操作)

如何在OCaml中实现这样的东西?我可能试图把这段代码翻译得过于字面:我使用的是OcAML函数数组,如C++。问题是,我的结局一直是这样的:

void op_plus(size_t pc, uint8_t* data) {
  *data += 1;
  BytecodeArray[pc+1](pc+1, data); //call the next operation in the array
}
let op_plus pc data = Printf.printf "pc: %d, data_i: %d \n" pc data;
                        let f = (op_array.(pc+1)) in         
                        f (pc+1) (data+1) ;;
let rec op_plus pc data = ...
and op_array = [| ... |]

其中,op_数组是在上面的范围中定义的数组,然后在以后重新定义它以填充一组op_plus函数。。。但是,op_plus函数使用前面的op_数组定义。这是一个鸡和蛋的问题。

您不应该重新定义
op\u数组
,您应该通过修改它来填充指令,使其与您的函数已经引用的
op\u数组
相同。不幸的是,您无法在OCaml中动态更改数组的大小

我看到两种解决办法:

1) 如果不需要更改“指令”序列,请使用数组
op_array
以相互递归的方式定义它们。OCaml允许定义从构造函数的应用程序开始的相互递归函数和值。比如:

void op_plus(size_t pc, uint8_t* data) {
  *data += 1;
  BytecodeArray[pc+1](pc+1, data); //call the next operation in the array
}
let op_plus pc data = Printf.printf "pc: %d, data_i: %d \n" pc data;
                        let f = (op_array.(pc+1)) in         
                        f (pc+1) (data+1) ;;
let rec op_plus pc data = ...
and op_array = [| ... |]
2) 或者使用另一个间接方法:将
op_数组
作为对指令数组的引用,并在函数中引用(!op_数组)。(pc+1)。稍后,在定义了所有指令之后,您可以使
op_array
指向一个大小合适的数组,其中包含您想要的全部指令

let op_array = ref [| |] ;;
let op_plus pc data = ... ;;
op_array := [| ... |] ;;
还有一个选项(如果事先知道大小)-首先用空指令填充数组:

let op_array = Array.create size (fun _ _ -> assert false)
let op_plus = ...
let () = op_array.(0) <- op_plus; ...
让op\u array=array.create size(fun\uu->assert false)
让op_plus=。。。

let()=op_数组。(0)另一种选择是使用CPS,并完全避免显式函数数组。尾部调用优化仍然适用于这种情况

我不知道如何生成代码,但让我们做出一个合理的假设,即在某个时刻,您有一组VM指令要准备执行。每个指令仍然表示为一个函数,但它接收的不是程序计数器,而是连续函数

以下是最简单的示例:

type opcode = Add of int | Sub of int

let make_instr opcode cont =
    match opcode with
    | Add x -> fun data -> Printf.printf "add %d %d\n" data x; cont (data + x)
    | Sub x -> fun data -> Printf.printf "sub %d %d\n" data x; cont (data - x)

let compile opcodes =
    Array.fold_right make_instr opcodes (fun x -> x)
用法(查看推断类型):

为了清晰起见,我用了两次传球,但很容易看出一次传球就能完成

下面是一个简单的循环,可以通过上面的代码编译和执行:

let code = [|
    Label "entry";
    Phi (((<) 0), "body", "exit");
    Label "body";
    Sub 1;
    Jmp "entry";
    Label "exit" |]
因此,即使在紧循环中,CPS的性能也比阵列要好得多。如果展开循环并将一条
sub
指令替换为五条指令,则数字会发生变化:

array: mean = 5.28 s, stddev = 0.065
CPS: mean = 4.14 s, stddev = 0.309
有趣的是,这两种实现实际上都优于OCaml字节码解释器。在我的机器上执行以下循环需要17秒:

for i = 500_000_000 downto 0 do () done

对于可调整大小的数组,可以使用ExtLib.DynArray或res,这是我最后采用的方法,因为数组的大小是程序中的指令数,并且大小是预先知道的。我还可以在解析过程中以编程方式填充数组,这是这种方法的一个优点。实际上,虽然这在REPL中起作用,但当我尝试使用ocamlc编译时,它不起作用,我得到:错误:此表达式的类型,('''u a->''u b->''u c)数组,包含无法从此行泛化的类型变量:let op_array=array.create code_size(fun u_u->assert false);;必须将其更改为:let op_array=array.create code_size(fun(x:int)(y:int)->Printf.Printf“Done.\n”);;有趣的是,另一个在REPL中工作。如果以这种方式实现直接线程解释程序,很快就会出现堆栈溢出:-)在标准C中无法实现直接线程解释程序,这就是GNU发明计算标签gotos作为编译器扩展的原因。@Lothar“stack overflow”->不在OCaml版本中。问题中对
f
的调用被编译为尾部调用。我差一点评论了一下,然后我决定这不是问题的主题。有趣。这将如何与某种条件跳转或“如果”操作码一起工作?请参阅更新。CPS转换和基于CPS的口译员已经得到了广泛的研究,你可以找到比我天真的方法更好的解决方案,但它仍然有效。