是否可以检查 SMLNJ 中的指针相等性(用于调试)?

kin*_*juf 6 reference sml smlnj memoization

假设我们有以下玩具二叉树结构:

datatype Tree = Leaf | Branch of Tree * Tree
fun left(Branch(l,r))= l
fun right(Branch(l,r))= r
Run Code Online (Sandbox Code Playgroud)

并假设我们有一些大而昂贵的计算树

val c: Tree= …
val d: Tree= Branch(c,c)
Run Code Online (Sandbox Code Playgroud)

我们能否在 SML/NJ 解释器中验证left(d)right(d)确实引用内存中的同一个位置?

(这个问题源于使用可能包含循环的惰性流,并试图调试记忆是否正常工作。)

Mot*_*oth 3

我认为我们可以通过将两个值转换为wordusing来实现这一点Unsafe.cast,它将指针重新解释为可以与 进行比较的数字=。这是一个is实现这个想法的函数:

infix 4 is (* = > < >= ... *)
fun op is(a: 'a, b: 'a) = (Unsafe.cast a: word) = Unsafe.cast b
Run Code Online (Sandbox Code Playgroud)

注意:

  • 我需要进行注释(a, b)以确保类型检查器将参数限制为同一类型,因为否则的 typesigis将是'a * 'b -> bool
  • 我需要注释第一个Unsafe.cast应用程序以防止 SML/NJ 必须使用polyEqual,从而避免发出Warning: calling polyEqual
  • 类型推断可以很好地解决剩下的问题

这是一个说明向量中结构共享的示例:

local
  fun const a _ = a
  val v = Vector.tabulate(1000, const #[1,2,3])
  val v = Vector.update(v, 230, #[1,2,3]) (* same value, but new allocation *)
in
  val test1 = Vector.sub(v, 0) is Vector.sub(v, 999)
  val test2 = Vector.sub(v, 0) is Vector.sub(v, 230)
end
Run Code Online (Sandbox Code Playgroud)

它确实按预期工作。回复者回答为

(* val test1 = true : bool
   val test2 = false : bool *)
Run Code Online (Sandbox Code Playgroud)

现在这是您问题中的树示例:

local
  datatype Tree
    = Leaf
    | Branch of Tree * Tree

  fun left (Branch(l,r)) = l
  fun right (Branch(l,r)) = r

  val c = Branch(Leaf,Leaf) (* imagine it being something complex *)
  val d = Branch(c, c)
in
  val test3 = left d is right d
end
Run Code Online (Sandbox Code Playgroud)

当我们尝试时,它会正确回答:

(* val test3 = true : bool *)
Run Code Online (Sandbox Code Playgroud)

我想这回答了你的问题。在这一点下面,我将讨论选择word以及当我们转换为它时内部可能发生的事情


据我所知,SML/NJ 像许多 lisp、v8、OCaml 等一样进行指针标记。这意味着我们想要专门转换为非堆分配的类型。因为我们希望能够读取指针值,不要误解堆对象。

我认为word对于这个目的来说效果很好;它像 int 一样是直接的,并且与它不同的是无符号的..所以它应该对应于内存地址(不要阻止我)。

似乎有一个 bug* 阻止您直接在 repl 中检查单词值,这可能是指针标记在起作用。
* 至少编译器是这样报告的?自 v110.99 起

一种解决方法是立即将值转换为不同的表示形式(也许需要装箱?),例如字符串或 Word64.word

fun addrOf x = Word.toString (Unsafe.cast x)
Run Code Online (Sandbox Code Playgroud)

事实上,当我们尝试使用新定义的addrOf函数来比较地址与其字符串化值时,我们可以观察指针标记的效果

(* We'll need these definitions onwards, might as well have them here: *)

infix 5 >> <<
val op >> = Word.>>
val op << = Word.<<
val unsafeWord = Option.valOf o Word.fromString
Run Code Online (Sandbox Code Playgroud)
local
  val x = SOME 31 (* dummy boxed value *)
  val addr = unsafeWord (addrOf x)
in
  val test4 = Unsafe.cast x = addr
  val test5 = Unsafe.cast x >> 0w1 = addr >> 0w1 (* get rid of lowest bit *)
end

(* val test4 = false : bool
   val test5 = true : bool *)
Run Code Online (Sandbox Code Playgroud)

那么,如果标签只是 SML/NJ 中机器字的最低位,就像在许多标记指针实现中一样,那么准确地说,指针应该是转换值向右移动一次,然后向左移动再次。

fun addrOf x = Unsafe.cast x >> 0w1 << 0w1
Run Code Online (Sandbox Code Playgroud)

我们进行这种看似转换的原因nop(记住,所有指针都是偶数)是因为它正确地标记了过程中的转换字值。
如果我们先左移然后右移,当强制指针变成正确的单词时,标签本身会找到第一个操作的值..这就是为什么我们先右移..从零向左移动会填充较低的值位,因此不会丢失有关地址的信息,但内部会正确存在立即值标记。

local
  fun strAddrOf x = Word.toString (Unsafe.cast x)
  fun isEven x = Word.andb (x, 0w1) = 0w0

  val x = SOME 42
  val ogAddr = unsafeWord (strAddrOf x) (* a known-correct conversion: no shifting takes place *)
  val badAddr = Unsafe.cast x << 0w1 >> 0w1
  val goodAddr = Unsafe.cast x >> 0w1 << 0w1
in
  val test6 = ogAddr = badAddr
  val test7 = ogAddr = goodAddr
  val test8 = isEven ogAddr
end

(* val test6 = false : bool
   val test7 = true : bool
   val test8 = true : bool *)
Run Code Online (Sandbox Code Playgroud)

这种移入addrOf允许您直接获取指针值,而无需中间转换(和装箱)到stringor word64。当然,此解决方案会因实际未装箱的值而崩溃,因此最好测试对象是否Unsafe.boxed在定义中以 ( )开头装箱addrOf,并在您使用立即数的情况下返回 0wx0 。

希望这适合您的目的。到目前为止,这对我来说确实如此!