Answers:
直观的答案是,如果您没有无限循环,没有递归并且没有goto,则程序将终止。这不是完全正确,还有其他方法可以潜入非终止状态,但是对于大多数实际情况来说已经足够了。当然,反过来是错误的,有些语言具有这些构造,它们不允许使用非终止程序,但是它们使用其他种类的限制,例如复杂的类型系统。
脚本语言的一个常见限制是动态地防止递归:如果A调用B调用C调用…调用A,则即使递归实际上终止,解释器(或您的检查器)也会放弃或发出错误信号。两个具体示例:
C预处理器在扩展该宏时会保留该宏。最常见的用途是为函数定义包装器:
#define f(x) (printf("calling f(%d)\n", (x)), f(x))
f(3);
这扩展到
(printf("calling f(%d)\n", (3)), f(3))
相互递归也被处理。结果是C预处理程序总是终止,尽管可能会以较高的运行时复杂度构建宏。
#define f0(x) x(x)x(x)
#define f1(x) f0(f0(x))
#define f2(x) f1(f1(x))
#define f3(x) f2(f2(x))
f3(x)
Unix shell递归地扩展别名,但是直到它们遇到已经被扩展的别名为止。同样,主要目的是为类似名称的命令定义别名。
alias ls='ls --color'
alias ll='ls -l'
一个明显的概括是允许递归深度最大为,其中可能是可配置的。ñ
有更多通用的技术可以证明递归调用终止,例如找到某个正整数,该整数总是从一个递归调用减少到下一个递归调用,但是这些很难检测。它们通常很难验证,更不用说推断了。
如果您可以限制迭代次数,则循环确实会终止。最常见的标准是,如果您有一个for
循环(没有技巧,即真正从到计数),它将执行有限次数的迭代。因此,如果循环主体终止,则循环本身终止。ñ
特别是,对于for循环(加上合理的语言构造,如条件),您可以编写所有原始递归函数,反之亦然。您可以在语法上识别原始递归函数(如果它们以不混淆的方式编写),因为它们不使用while循环,goto或递归或其他技巧。保证原始递归函数会终止,并且大多数实际任务都不会超出原始递归。
请参阅Terminator和AProVe。他们倾向于依靠试探法,而且我不确定他们是否清楚地描述了他们工作的程序的类别。尽管如此,它们仍被认为是最先进的,因此它们应该是您的良好起点。
是的,有可能。解决此类问题的一种常用方法是,根据代码作为输入的一部分来考虑额外的(单调)不可计算的参数。具有该参数的问题的复杂性可以大大降低。
我们无法计算参数,但是如果您知道要处理的输入实例具有较小的参数值,则可以将其固定为较小的数量并使用算法。
在形式化方法中使用此技巧和类似技巧来处理无法确定的暂停和类似问题。但是,如果您要决定的事情很复杂,那么算法的复杂性就不可能比在那些实例上运行算法更好。
关于另一个问题,如果您对输入进行足够的限制,则停顿问题会变得很容易。例如,如果您知道输入是多项式时间算法,则确定它们的停止问题是微不足道的(因为每个多项式时间算法都停止)。
形式方法中出现的问题通常是无法确定的,您可能需要查看有关它们如何在实践中解决这些问题的文献。
这不是一个形式上严格的答案,但它可以解决:
确定它是否永久停止或循环的问题。一次或在数字间隔之间循环一个有限集合是可以的。编辑:显然,只有在迭代(或至少禁止增长)迭代集合或间隔被禁止更改(例如,通过不变性)的情况下,这才起作用。
递归可能不行,除非您设置一个人工规则使其有限,例如允许最大堆栈深度,或在每次迭代中强制非负参数减少。
随意的东西通常是不好的。向后抛弃很可能导致无限循环。
Whiles和do-whiles语句是一个问题,因为它们取决于在执行期间不保证更改或不更改的条件。一种可能的(但可能非常不令人满意)的限制方法是给出最大可能的迭代次数。
您需要提供脚本语言的定义,以及脚本编写者“期望”的含义。
如果语言只允许线性赋值和线性条件,那么在我的评论中,这种停止可以在多项式时间(或多或少)中确定。
Aaron R. Bradley,Zohar Manna和Henny B. Sipma对一类多项式程序得出类似的结果。但是AFAIK(在这里我可能错了),运行时是双指数的(本质上是计算Groebner基所需的时间)。