11
$\开始组$

支持链式比较运算符的最明显方式,例如a<b<c,将其重写为(a<b)和(b<c).这就是我所做的在我的AEC-to-WebAssembly编译器中,用C++编写:

}else if(text==“<”或text==”>“或text===”<=“或text=”>=“){if(childrens.at(0).text)==text)//链式比较,例如“a<b<c”。{TreeNode和Node(“and”,lineNumber,columnNumber),secondChild(text,lineNumber,columnNumber);secondChild.children={childrens.at(0).childens.at(1),children.sat(1”)};andNode.childrens={childrens.at(0),secondChild};标准::cerr<<“Line”<<lineNumber<<“,Column”<<columnNumber<<“,编译器警告:未实现链式比较”“在这个编译器中是正确的。在这种情况下,中间项是\”<<childrens.at(0)children.at(1).text<<“\”,将被评估两次,可能会导致意外的结果““影响。对此我很抱歉,但到目前为止,还没有”“考虑到编译器的方式,这似乎是一个简单的解决方案”“结构化的。我已经开始了一个StackExchange线程”“问题:https://langdev.stackexchange.com/q/3755/330"<<标准::endl;return“;链式比较,转换”+getLispExpression()+“到”+andNode.getLispExpression()+“\n”+andNode.compile(上下文);}

然而,这有一个问题。假设b条是一个有副作用的函数。正如我写的那样,编译后的代码将调用它两次,而不是一次。

例如,这个AEC课程:

#目标WASI整数16计数器:=0;函数b()返回整数32计数器+=1;返回计数器;结束函数函数test()返回整数32//根据常识,这应该返回1。然而,由于//当前AEC语义,返回2。计数器:=0;整数16 a:=0,c:=2;整数16 resultOfComparison:=a<=b()<=c;返回计数器;EndFunction(结束函数)

如果使用调用这个JavaScript程序:

const fs=要求(“fs”);const buffer=fs.readFileSync(“chainedComparisonWithSideEffects.wasm”);WebAssembly.installate(buffer).then((results)=>{const exports=results.instance.exports;console.log(“测试返回:”+exports.test());});

它输出测试返回:2而不是测试返回:1.

那么,如何正确编译它呢?

$\端组$
5
  • 7
    $\开始组$ 对表达式求值一次并使用两次?为什么你需要一棵树而不是DAG? $\端组$ 4月29日12:32
  • 5
    $\开始组$ 如果您在这个阶段真的需要使用表达式树而不是DAG,那么您可以将其推迟到使用以下内容的webasm DAG时设a<temp和temp<c中的temp=b如果您可以在表达式树中创建一个匿名临时。 $\端组$ 4月29日12:35
  • 4
    $\开始组$ 为了获得正确的语义,您(大概)必须计算之前b条,因此必须同时使用局部变量。CPython可以通过使用堆栈指令来避免局部变量DUP_TOP(DUP_TOP)旋转螺纹,但不幸的是WebAssembly没有这些。 $\端组$
    – 卡亚3
    4月29日13:29
  • $\开始组$ @user1937198什么是DAG? $\端组$ 5月1日7:24
  • $\开始组$ @FlatAssembler定向非循环图:en.wikipedia.org/wiki/Directed_acyclic_graph这允许表示价值观可能会被多次消费,这是一个非常常见的愿望。 $\端组$ 5月1日10:44

4个答案4

重置为默认值
12
$\开始组$

老生常谈的答案是,如果评估b条两次是错误的,那么只应该评估一次。。。

免责声明:我不知道AEC,所以我将编写语法。

是否脱糖

为了回答这个问题,我将AEC的去糖化定义为语义等价的AEC代码的转换。

我想问的第一个问题是你是否应该去糖。

去语法化的优点是可以尽早删除结构,从而可能简化编译器的其余部分,但它也有自己的挑战:诊断应该引用原始源代码,含义不应该改变,有时根本不可能用语言表达目标语义。

因此,如果您由于任何原因无法取消糖化,请不要这样做。请将问题转移到编译管道的稍后部分,这样就不会受到取消糖化所面临的限制。

如果你真的想脱糖,请继续阅读。

变量

去糖化需要引入变量,这绝对不应影响任何现有变量。

作弊是一个可能有用的伎俩。与C和C++一样,为“实现”保留前缀允许使用@1,@2,等等……当其他人都不被允许时。否则,您需要一组范围内变量名,并且需要避免使用它们。

使用块表达式

如果语言有块表达式1,您本质上想替换a’<b’<c’通过以下方式:

{设@1=a';设@2=b';@1<@2&&@2<c'}

(其中a’和co是表达式的位置所有者)

如果有更多元素,请嵌套:

{设@1=a';设@2=b';@1 < @2 && {设@3=c';@2 < @3 && {设@4=d';@3 < @4}}}

块表达式是最好的,因为即使在Rust这样的语言中,输入和借用都非常严格,它们也可以避免拼写出类型、借用模式等。。。

1 块表达式是将块用作表达式的能力.

有一个封口

在没有块表达式的情况下,最简单的方法是引入闭包:

定义closure_42():@1=a'@2=b'如果不是(@1<@2):return错误@3=c’如果不是(@2<@3):return错误#可能还有其他表达return True

然后用闭包替换替换的表达式。

使用函数

在没有闭包的情况下,您可以模拟它。。。

您实际上创建了一个函数,该函数将表达式中引用的所有变量的并集作为输入进行替换。这在动态类型语言或带有模板的语言中效果良好。

def生成_42(v0',v_1',v_2',v_3'):@1=a'@2=b'如果不是(@1<@2):return错误@3=c’如果不是(@2<@3):return错误#可能还有其他表达return True

其中v_0',v_1',v_2'。。。是定义a'、b'和c'的作用域的变量,由a'、b'和c'引用。请注意,每个引用变量只需传递一次,它们的名称应该与以前相同。

现场

最后也是最起码的解决方案是现场重写整个的条件。

这是我最不喜欢的,因为它需要重写整个表达式,而不仅仅是子表达式a’<b’<c’,或者至少将表达式的前缀重写为要重写的最后一个子表达式。这使得重写变得不那么局部,因此正确地执行会更加烦人,并且当在单个表达式中重写多个子表达式时,情况会变得更糟。

除此之外,它不能使用短路。

之前:

如果foo()和a'<b'<c'。。。和bar():#原始块。

之后:

@条件=foo()如果@条件:@1=a'@2=b'@条件=@1<@2如果@条件:@3=c’@条件=@2<@3#进一步链接if@condition和bar():#原始块。

我确实注意到了一个不幸的问题:必须重写整个条件,或者至少重写前缀如果你觉得有趣的话。

$\端组$
10
  • $\开始组$ 即使该语言没有块表达式,也可以使用包含它们的扩展中间表示。 $\端组$ 4月29日17:28
  • 1
    $\开始组$ 两种方法都只计算一次。为什么要将其包含在引入的变量列表中以防止双重评估? $\端组$
    – 非合金
    4月30日2:15
  • 2
    $\开始组$ @阿玛洛伊:因为之前需要评估设b=b';,但随后使用。 $\端组$ 4月30日6:00
  • 1
    $\开始组$ 对于一个潜在的PL实现新手提问者来说,基于这个问题,我认为需要详细说明这一点。或者,函数封装可以很好地使用惰性求值或按名称传递参数 $\端组$ 4月30日14:04
  • 1
    $\开始组$ @菲尔米勒:老实说,我不认为这是必要的。。。。但它不会占用太多空间,所以为什么不呢。 $\端组$ 4月30日14:42
5
$\开始组$

因此,这取决于您的操作语义,特别是您的语言是显示Lazy还是Eager Evaluation。
也就是说,请记住,即使在大多数急切求值的语言中,布尔连接和析取运算符()表现出懒惰的评价。

x∧y有时意味着假∧(x/0)合法且等同于,但有时这是非法的。
这将在以下方面变得重要。

记忆和重写

这一点很简单:只需将比较链重写为连词或析取。

x<y<z成为x<y∧y<z.
然后,假设“懒惰评估”(Lazy Evaluation),这进一步降低为力x<力y∧力y<力z每个术语都会被记住。
假设急切评估使用临时词明确表示如下:
设x'=/*力*/x;;y'=/*力*/y,单位为x'<y'∧y'<z.

现在,什么3<0<爆炸()应该做的是取决于布尔连接是否懒惰。

二进制比较不会产生布尔值

这个有点时髦,但在现有语言中有效,并且依赖于二进制比较运算符返回一个既可以转换为布尔值又可以在比较链中使用的类型。
它很简单:它保存了对RHS的引用,并让操作符与RHS进行比较。

下面是C++中说明的方法:

模板<类型名T>结构比较变量{常数布尔r;常数T&rhs;运算符bool()常量{返回r;}比较变量<T>运算符<(const T&arg)const{return比较变量<T>{r&&(bool)(rhs<arg),arg};}};结构Int{整数val;比较变量<Int>运算符<(const Int&arg)const{return比较变量<Int>{val<arg.val,arg};}};/**/extern“C”int printf(常量字符*,…);int main(int argc,char*argv[]){积分x={1},y={2},z={3};布尔fst=x<y<z;布尔snd=z<y<x;printf(“%d%d”,fst,snd);}

x<y<z(x.Int::运算符<(y))。比较变量<Int>::运算符<(z),其中,正如您所看到的,每个操作数只出现一次。

$\端组$
4
$\开始组$

你已经得到了一些很好的答案,但我要指出的是,我不得不这样做两次,并且用了两种不同的方式;两者都有效。

方法一:

当我在Facebook上编写Python-to-Python编译器时,我们重写了r=a()<b()<c()通过以下步骤:

  • 第一个操作数可能有副作用吗?如果没有,我们就完了。如果是,请改写为_a=a();r=_a<b()<c().

  • 第二个操作数可能有副作用吗?类似地改写,现在我们有_a=a()_b=b();r=_a<_b<c()

  • 有不止一个操作员吗?如果是,现在可以安全地将赋值重写为r=_a<_b和_b<c().

方法二:

C#没有这种链式比较,但它有许多其他表达式,其中规范说“……但表达式只计算一次”。我们的中间形式在解析后但在IL生成之前有一对特殊节点:一个是指“计算refered-to表达式并将其存储到temp”,另一个是“在refered-to-store表达式中获取temp的值”。这使得降下的物体成为DAG而不是树,但这没什么大不了的。然后,IL生成器决定是否将temp转换为未命名的本地指令或dup指令或其他指令。

两者都有效。在第一种情况下,我已经在编写Python-to-Python编译器,所以完全用Python进行降低是有意义的,在第二种情况下,有一个自定义的中间格式更容易,我们可以稍后进行特殊情况处理;做最适合你的应用程序的事情。

$\端组$
1
$\开始组$

你可以把它翻译成

(a<(温度b:=b))和(温度b<c)

在代码生成之前。温度_b应该是编译器生成的唯一标识符。对链接比较中的每个步骤重复此替换。

$\端组$

你必须登录来回答这个问题。

不是你想要的答案吗?浏览已标记的其他问题.