【问题标题】:Can Common Lisp type annotations result in unsound behavior?Common Lisp 类型注释会导致不合理的行为吗?
【发布时间】:2015-07-01 20:06:40
【问题描述】:

我知道如果安全设置较低,Common Lisp 可以使用类型注释作为优化辅助,并且不被检查。例如,该程序运行并打印数字和字符串,没有任何类型错误。 (当安全 >= 1 时,我只会在 SBCL 中收到类型错误)

(declaim (optimize
           (speed 3)
           (safety 0)))

(defun f (x)
  (declare (type fixnum x))
  x)

(format t "1 ~A~%" (f 17))
(format t "2 ~A~%" (f "asd"))

我现在想知道如果安全性设置为零并且不遵守类型注释,是否有可能创建一个执行令人讨厌的事情的程序。诸如从一种类型转换为另一种类型(如 C 类型转换)和其他类型的未定义行为。

到目前为止,我还没有找到一个可以做到这一点的例子。我尝试了使用类型化数组的这个示例的变体,但它们都没有导致类型转换行为。

(declaim (optimize
           (speed 3)
           (safety 0)
           ))

(defun f ()
    (let ((arr (make-array '(5)
      :element-type 'fixnum
      :initial-contents (list 1 2 3 4 5))))
      (declare (type (vector fixnum 5) arr))
      (setf (aref arr 0) "hello")
      (aref arr 0)))

(format t "a1 ~A~%" (f))

在 CLISP 中,该程序打印“hello”,而不对 int 进行类型转换,而在 SBCL 中,程序中止并出现 SIMPLE-TYPE-ERROR 错误。

如果我不尊重我的类型声明,有没有办法创建一个会导致恶魔从我鼻子里冒出来的 Common Lisp 程序?

【问题讨论】:

  • 如果这是一个恶魔,我不知道,但在 CCL 中,您的程序会打印:a1 6614253635515
  • 对于 SBCL,类型推断知道本地创建值的类型。 sds 的回答显示了解决这个问题的好方法。
  • @Xach:你能举个例子吗?在 SBCL 上运行 sds 的示例给了我一个 #(1 2 3) is not a LIST 错误,而不是我想要的无意义值。

标签: types common-lisp declare


【解决方案1】:

Common Lisp 标准说有类型声明。它并没有真正说明他们做了什么或实现会对他们做什么。

目的:

  • 生成类型特定代码
  • 运行时和/或编译时类型检查。这主要由 CMUCL、SBCL 和 SCL 完成。

假设我们有操作:

(+ i 100)

默认情况下它是一个通用的+,它可以处理所有的数字类型。

如果我们告诉ifixnum 那么

  • + 操作可以是特定于 fixnum 的

如果另外将返回类型声明为fixnum

  • 它可能不会溢出到 bignum 中

如果我们额外告诉编译器我们想要低 safety,那么编译器将不会生成运行时类型检查。

编译器提供的功能没有标准化。它甚至可以完全忽略类型声明。

如果你有一个支持特定类型代码的编译器(并且有很多这样的编译器),安全性很低,并且在运行时提供了错误类型的对象,那么它可能会产生不良后果。包括由于堆内存损坏而导致 Lisp 崩溃。

因此,最好只在非常小的代码区域而不是整个文件或系统上使用低安全性。使用带有locally 的声明会有所帮助。

SBCL(也称为 SCL 和 CMUCL)是特殊的,因为它还将类型声明视为类型检查的断言。

【讨论】:

  • 关于“它可能不会溢出到一个bignum”,它实际上可能。您的实现可能具有参数到返回类型的传染,这并非闻所未闻,但它并不正确。对于这个效果,你应该声明结果的类型,比如(the fixnum (+ i 100)),假设i已经被声明为fixnum类型。
  • @PauloMadeira:是的,这更有意义。我已经稍微改变了答案。
  • 您能否提供一个崩溃或最终导致内存损坏的示例?到目前为止,我尝试的所有代码在有和没有类型声明的情况下都表现得完全相同(这意味着类型声明是无用的)
【解决方案2】:

以下是 sbcl 中 safety 0 的几个示例:

CL-USER> (proclaim '(optimize (safety 0)))
; No value

越界读取

CL-USER> (loop for i from 0 to 20 
              do (print (aref #(0) i)))

0 
0 
#(0) 
(I) 
I 
NIL 
(AREF #(0) I) 
NIL 
(PRINT (AREF #(0) I)) 
NIL 
#<unknown immediate object, lowtag=#b1001, widetag=#x59 {D59}> 
#<SB-KERNEL:LAYOUT for SB-KERNEL:LEXENV {10005F3C33}> 
NIL 
NIL 
NIL 
NIL 
NIL 
NIL 
NIL 
NIL 
NIL 
NIL

未定义的类型转换

CL-USER> (defun f (x y)
           (declare (fixnum x y))
           (+ x y))
F
CL-USER> (f t ())
537919538

在 SBCL 中,如果您闻到鼻恶魔的味道,您可以在加载有问题的代码之前重新启动图像并输入 (sb-ext:restrict-compiler-policy 'safety 3)(也可能再次输入 'debug)。运气好的话,你会得到一个很好的条件,而不是未定义的行为。

【讨论】:

  • 越界数组读取是未定义行为的一个很好的例子,但实际上与类型注释无关,这是我最好奇的。你知道是否有可能编写一个带有和不带类型注释的行为不同的程序(假设安全=0)
  • @hugomg f 如果您删除类型声明并进行相同的调用,则会发出类型错误信号。
  • @hugomg 我们使用了相同的函数名——我的意思是我的示例末尾的f。那是您正在寻找的那种类型转换吗?
  • 是的,这正是我想要的!我第一次没有注意到它,因为我没有注意并认为它是 for 循环打印的废话行的一部分。
  • SBCL 恶魔:(defun foo (x) (declare (optimize (safety 0)) (type cons x)) (car x)),然后是(foo 0)
【解决方案3】:

不确定这些恶魔,但您可以得到一个段错误,就像我在 15 多年前使用 CMUCL 和类似以下代码所做的那样:

(declaim (optimize (speed 3) (safety 0)))
(defun f (v)
  (declare (type (vector double-float) v))
  (loop for x in v sum x))
(f #(1 2 3))

请注意,我承诺我只会将(vector double-float) 传递给f,然后我给了它一个simple-vectorfixnums。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-24
    • 2014-10-24
    相关资源
    最近更新 更多