本苏打算去看看盗版《天气之子》之类的动漫电影什么的,
在此之前,本苏不得不先谈谈解释器实现时的作用域实现 #CS #PLT
下文,我们涉及到主要术语:子程序(subroutine)、参数(argument)、局部变量(local variable)、上值(up-value)、闭包(closure)、词法作用域(lexical scoping)、动态作用域(dynamic scoping)
我们要处理的,就是实现局部变量,此外作为子程序参数的局部变量也包含在内。
此外,还有闭包——对闭包的创建有点像面向对象的架构器调用,我们需要包住 UpValue——也就是外部函数(outter function) 的局部变量,之后闭包被展开(调用,或者说求值)的时候可以用到包好的值。
你和 Ada 通过 SICP 和龙书了解了有这么两种作用域方案(划掉,怎么可能这么简单)
(-1). (大脑降级方案)使用全局作用域
0. (不全实现方案)使用 Map<String, Perfixed<String>>,给变量加函数名前缀。
1. 使用时序作用域,也即动态作用域。 你们用 Stack<Map<String, Object>> 存储变量,每一层都是
2. 使用 Map<String, StackOrSingle<Object>> 优化方案
3. 使用 pre-bind 预先指定使用到的变量,然后在进入可能修改变量的子程序前把老变量备份在系统栈的 Map 上,速度更快。
但是 Ada 同学很快否定了这些做法,理由是,那样容易造成混淆。
咱知道,子程序可以是递归的,也就是说,属从它的参数可能在同一时间有很多个,上面也说了。
但关于子程序
但除了参数,局部变量也是要有的,怎么办?
如果程序自己调用自己也是可能的,怎么办?
调用栈最深的时候,just 的参数是这样的:
所以,为了保证递归可能性,作用域的实现必须和栈有关。
那么局部变量怎么办呢?也在栈上分配嘛。
变量是什么?最关键却不太容易感知到的一点是,变量就是针对某个存储空间的,名字,或者标记符而已,它后面藏着『一个』——有且只有一个 存储空间。
就好像
也就是 lambda 演算的世界观,不过这里才不会提太多术语呢!上面的
我们从
而从
这整个动作,则被称为『施用 Apply』,整个程序表达呢?就叫『应用Application』。
所以参数、局部变量、闭包该如何实现呢?(当然也有全局变量)
Lua呢,因为是手动递归下降解析器,所以直接在解析的时候,做好这个问题就完了。不就是一个名字对应一个存储空间,冲突按嵌套次序深者优先么。
这个问题怎么解决呢?每一层解析器,都得知道当前有什么参数,方便统一这个存储位置。
最根本的需要就是一个
局部变量呢?当然,参数就是局部变量,不过是可以暴露出来给调用者提供的那种而已。
局部变量,定义的时候直接在栈帧上分配好了存储空间。至于都放在栈上如何不冲突呢?
这就涉及到调用约定的问题,调用者知道如何操纵咱共同的存储——系统栈,不会破坏要执行的子程序的取参行为。
名字冲突呢?不存在的事情,字符串的名字可能冲突,存储空间是不可能冲突的。
名字如何在内部的闭包中共享呢?
让解析器知道,闭包里的
不过这有点区别——其实也可以作为引用实现,假设某函数把一个包着自己变量的闭包递了出去,就有可能在变量还『活着』的时候,被自己调用的函数修改,我们这里不考虑这种情况。
Lua 的实现是,UpValue(也就是上面我们说的 someVar,某个闭包包住的外层函数变量)
有两个状态:open/close,我忘记它们代表什么了,好像和 GC 有关。
于是一个闭包长这样:
调用者知道的事情,就是按顺序把它们压到栈上,一般是后参先压(我是指传统 CDEF 的,Lua、JVM 里可能不是这样)。
然后子程序知道的事情,就是现在 (a,b) => a+b 里的 a,我们到栈的第一个位置取,这就保证了 a 指代同一个位置,并且符合参数传递的预期。
局部变量也是一样,都商议好分配位置,如知道
于是,我们的参数可以传递,并且有名字、局部变量可以读写,并且也有名字。现在闭包对 UpValue 的引用怎么办?
我们觉得,既然闭包不可避免地是一种数据对象,那就给它安排一个数组,来存放其 capture 住的部分,其他的和普通函数无二,也有自己的代码、参数和局部变量。
创建闭包时,检查闭包的 captures 列表,分配一个数组把 capture 住变量的实际值都存到里面去,合并入闭包对象的状态里,这样运行时就能拿到 UpValue 的信息了。
—
实现注记:
1. Subst 和 Variable 都是 Local,但 Subst 没有 setValueIn。
2. Upvalue 和 Local 都是 Resolvable,能够关于一个栈取到值
3. Function 和 Closure 都是 Subroutine,可以被提供参数调用也可以有局部变量
4. 在 Lua 里,函数是有 maxlocals 的,换句话说,Subroutine 就有使用的局部存储空间大小这一说,可以在运行前确定。
5. 解析器可以选择低内存优化和低计算优化
如果要为低内存优化,可以使用 ArrayMap,再加上 base map, shadowed map 先往局部遮盖的符号(可能没有)里找,再往上一层的遮盖找,这个数据和算法可以封装为一个 class,一般情况都不用。
6. 查找顺序:局部变量、参数、外部函数的 UpValue,否则就是全局变量。
在此之前,本苏不得不先谈谈解释器实现时的作用域实现 #CS #PLT
下文,我们涉及到主要术语:子程序(subroutine)、参数(argument)、局部变量(local variable)、上值(up-value)、闭包(closure)、词法作用域(lexical scoping)、动态作用域(dynamic scoping)
在编程中,我们经常……算喽,简略点。
我们要处理的,就是实现局部变量,此外作为子程序参数的局部变量也包含在内。
此外,还有闭包——对闭包的创建有点像面向对象的架构器调用,我们需要包住 UpValue——也就是外部函数(outter function) 的局部变量,之后闭包被展开(调用,或者说求值)的时候可以用到包好的值。
function makeCounter() {
var count = 0;
return () => { count++; };
}
var counter = makeCounter()
counter() //: 0
counter() //: 1
假如你是李华,你的朋友 Ada 想要创建一门编程语言,但她不知道如何提供作用域,以使得程序表达更模块化。你和 Ada 通过 SICP 和龙书了解了有这么两种作用域方案(划掉,怎么可能这么简单)
(-1). (大脑降级方案)使用全局作用域
0. (不全实现方案)使用 Map<String, Perfixed<String>>,给变量加函数名前缀。
1. 使用时序作用域,也即动态作用域。 你们用 Stack<Map<String, Object>> 存储变量,每一层都是
Map<String,Object>,然后每次取值时用名字往外找,第一个匹配的就是值,注意,如果你们用 String 作为名字(标识符),不同函数如果名字一样会导致产生可跨层引用,对返回函数指针的处理也可能遭致问题,因为子程序和变量值是完全分开的。2. 使用 Map<String, StackOrSingle<Object>> 优化方案
3. 使用 pre-bind 预先指定使用到的变量,然后在进入可能修改变量的子程序前把老变量备份在系统栈的 Map 上,速度更快。
但是 Ada 同学很快否定了这些做法,理由是,那样容易造成混淆。
one = 1 # "one" in add1那么咱来看 C++ 的 caputre:
def add1(n):
return one + 1
def each_people(people, fn):
for one in people: # most recent definition of "one"
return fn(one)
def print_grow1(person):
name, age = person
print(name + " aged " + add1(age)) # here
each_people([('Jack' 22), ('Rose', 19)], print_grow1)
[some_upvalue](int arg1) { return arg1+some_upvalue; } 咱知道,子程序可以是递归的,也就是说,属从它的参数可能在同一时间有很多个,上面也说了。
但关于子程序
(x) => x + 1 最关键的其实是,我们知道 (x+1) 里的 x 是这个东西的第一个参数。但除了参数,局部变量也是要有的,怎么办?
如果程序自己调用自己也是可能的,怎么办?
def just(n):
if n == 0: return 0
else return just(n-1)+1
just(2) //= just(1) +1 //= just(0) + 1 + 1 调用栈最深的时候,just 的参数是这样的:
[(2), (1), (0)] Top 所以,为了保证递归可能性,作用域的实现必须和栈有关。
那么局部变量怎么办呢?也在栈上分配嘛。
变量是什么?最关键却不太容易感知到的一点是,变量就是针对某个存储空间的,名字,或者标记符而已,它后面藏着『一个』——有且只有一个 存储空间。
就好像
f= (x, y) => x+y 里的 x, y 在生存周期上是『局部变量』,实际上是『参数』,再细化一点,x 代表第一个形式化参数、y 则是第二个。f(1, 2) 最终就代表着 (1+2),这才是正常一点的世界观。也就是 lambda 演算的世界观,不过这里才不会提太多术语呢!上面的
(x, y) 被称为 formals,形式参数、 (x+y) 被称为 body,体。我们从
(x, y) => x+y 的『抽象 Abstraction』,给定两个实际参数 (1,2),填充到 (1+2) 的过程被称为『替换 Substitute』,这 x, y 就被叫做『代词 Substitution』(我自己翻译的……)而从
(1+2) 算出最终的结果 3 的过程,被称为『收束 Reduce』(也是我自己翻译的…… 好像也叫『归约』)这整个动作,则被称为『施用 Apply』,整个程序表达呢?就叫『应用Application』。
所以参数、局部变量、闭包该如何实现呢?(当然也有全局变量)
Lua呢,因为是手动递归下降解析器,所以直接在解析的时候,做好这个问题就完了。不就是一个名字对应一个存储空间,冲突按嵌套次序深者优先么。
这个问题怎么解决呢?每一层解析器,都得知道当前有什么参数,方便统一这个存储位置。
最根本的需要就是一个
Map<Symbol, Local> 符号表,代表本层的词法作用域信息(之前所有外层嵌套的参数和局部变量)局部变量呢?当然,参数就是局部变量,不过是可以暴露出来给调用者提供的那种而已。
局部变量,定义的时候直接在栈帧上分配好了存储空间。至于都放在栈上如何不冲突呢?
这就涉及到调用约定的问题,调用者知道如何操纵咱共同的存储——系统栈,不会破坏要执行的子程序的取参行为。
function solve(a, b, c)(注:Lua 包含常量折叠,也就是说 d 这个数值常量可能不会作为局部变量,这里忽略这种可能)
d = 233
return a, b, c, d -- (arg0, arg1, arg2, var0)
end
名字冲突呢?不存在的事情,字符串的名字可能冲突,存储空间是不可能冲突的。
名字如何在内部的闭包中共享呢?
让解析器知道,闭包里的
someVar 实际上代表着外部 someVar 的值,就对了。不过这有点区别——其实也可以作为引用实现,假设某函数把一个包着自己变量的闭包递了出去,就有可能在变量还『活着』的时候,被自己调用的函数修改,我们这里不考虑这种情况。
Lua 的实现是,UpValue(也就是上面我们说的 someVar,某个闭包包住的外层函数变量)
有两个状态:open/close,我忘记它们代表什么了,好像和 GC 有关。
于是一个闭包长这样:
[captures](formals) {body}
我们怎么创建它呢?考虑一下实现的细节——每个参数都是 Subst(val idx: FrameIdx, val name: String)、每个局部变量都是 Variable(同上),区别主要是参数不可被赋值,而这个存储(编号)分配的过程在解析阶段完成。调用者知道的事情,就是按顺序把它们压到栈上,一般是后参先压(我是指传统 CDEF 的,Lua、JVM 里可能不是这样)。
然后子程序知道的事情,就是现在 (a,b) => a+b 里的 a,我们到栈的第一个位置取,这就保证了 a 指代同一个位置,并且符合参数传递的预期。
局部变量也是一样,都商议好分配位置,如知道
x 是在当前栈顶 -1,取/置都按照这个位置来即可。于是,我们的参数可以传递,并且有名字、局部变量可以读写,并且也有名字。现在闭包对 UpValue 的引用怎么办?
我们觉得,既然闭包不可避免地是一种数据对象,那就给它安排一个数组,来存放其 capture 住的部分,其他的和普通函数无二,也有自己的代码、参数和局部变量。
UpValue(val idx: Idx, val name: String)解析闭包时,记住外层函数的局部变量表,如果知道闭包引用了外层的东西,就把这个被引用的东西往 captures 列表一放,再对应地把对它的引用视为
UpValue(idx, name)。创建闭包时,检查闭包的 captures 列表,分配一个数组把 capture 住变量的实际值都存到里面去,合并入闭包对象的状态里,这样运行时就能拿到 UpValue 的信息了。
—
实现注记:
1. Subst 和 Variable 都是 Local,但 Subst 没有 setValueIn。
2. Upvalue 和 Local 都是 Resolvable,能够关于一个栈取到值
3. Function 和 Closure 都是 Subroutine,可以被提供参数调用也可以有局部变量
4. 在 Lua 里,函数是有 maxlocals 的,换句话说,Subroutine 就有使用的局部存储空间大小这一说,可以在运行前确定。
5. 解析器可以选择低内存优化和低计算优化
如果要为低内存优化,可以使用 ArrayMap,再加上 base map, shadowed map 先往局部遮盖的符号(可能没有)里找,再往上一层的遮盖找,这个数据和算法可以封装为一个 class,一般情况都不用。
6. 查找顺序:局部变量、参数、外部函数的 UpValue,否则就是全局变量。
Forwarded from Mr. Bug | Wild Pointer
Twitter
Michael Anti
青岛警方第一天抓造谣者,然后青岛政府凌晨把谣言官方再宣布下。现在疫情如此严重,各地警方能否少点二逼作孽,多按中央政府的命令救人?
Forwarded from Deleted Account
Wave.kt
728 B
#Kotlin 你说的好有道理啊,虽然花了很长时间我才知道 index/44100.0 再 %1.0 是什么意思,(time/ratio)*2*Pi*Short.MAX_VALUE 嘛,我之前也听说过 PWM,不过只知道能用来做 LED 的闪烁走马灯
audiooutput
67 KB
这个蛮好玩,就是
qCos(2*M_PI * tpos * sampleRate / format.sampleRate() * pwmRatio)Forwarded from 永久封存 | Yuuta 台 | 😷 #Pray4Wuhan (Yuuta | Nya⠀)
Telegram
綺凜的随波逐流
Forwarded from 永久封存 | Yuuta 台 | 😷 #Pray4Wuhan (玉米狐狸)
Telegram
情况正在变化
图转
/tmp/duangsuse.sock
#China #Low
#PLT 咳咳,咱来谈谈这个东西的实践的问题。
既然咱说是实践,那干脆就引入一个编程语言吧,我们叫它 MiniScheme。
Scheme 呢…… 不错!我们使用满是括号的 s-expression,这种基础表达方式。
那么这个语言呢,我们打算用 Kotlin Common 开发(因为也没必要特别地放 JVM 上)
Literate Programming…… 算了,LKT 还没完成,在那之前,还是保持老编程风格好了。
怎么计算?慢慢看,但我们认为,输入代码是
怎么实现那种按 Enter 就求值的 REPL 呢?我们仿冒一下 Firefox Developer Console 和 IRB 的设计:一个表达式可以写在多行,就是说换行时如果输入不完整可以继续读下去。
实现简单,将会使用到
我们的任务是什么呢?
+ 支持
+ 支持 Lexical Scoping,当然也包括闭包这个东西
+ 支持 null Name 以及 Boolean Int Long 的字面表达,注意 Int Long 也包含二进制和十六进制表示法
稍后也会支持 Float Double Char String 的字面表达,以及
+ 支持 () 括号,<> 括号稍后会被我们用于语言扩展,以下我们会把它作为分词过程的语言构词,保留词。
此外,为了语言的一致性,我们要禁止对 [] 和 {} 的使用。
大家觉得,
每一个 (+)、(*) 都代表一颗『语法树』的子树,它们都有值,比如
就是这样,那我们的语言怎么计算呢?或者说它怎么知道
参考一下上面的全局作用域吧,我们在全局作用域提供
我们可以把全局作用域做成一个 class,包含,为了语言的灵活性,还得有一个 getFunction 方法。 算喽,既然都是动态类型,动态在全局作用域取那些函数,就检查强转吧。
过会我们会通过简单的面向对象子类型多态 override 入门,认识 Visitor Pattern,写关于 evaluate, dump, check 这类操作的 AST Walker。
— 稍后的语言扩展 —
+ 支持 call-by-need,对参数进行惰性求值
现在我们是 call-by-value,在调用前对参数进行求值。
+ 可利用
+
待会我也会讲一下 Trie 树的实现,这样对可扩展关键字的实现,也可以自己思考下。
既然咱说是实践,那干脆就引入一个编程语言吧,我们叫它 MiniScheme。
Scheme 呢…… 不错!我们使用满是括号的 s-expression,这种基础表达方式。
(+ 1 2) ; 3 那么这个语言呢,我们打算用 Kotlin Common 开发(因为也没必要特别地放 JVM 上)
Literate Programming…… 算了,LKT 还没完成,在那之前,还是保持老编程风格好了。
怎么计算?慢慢看,但我们认为,输入代码是
CharIterator,这样支持 REPL 会比较方便。怎么实现那种按 Enter 就求值的 REPL 呢?我们仿冒一下 Firefox Developer Console 和 IRB 的设计:一个表达式可以写在多行,就是说换行时如果输入不完整可以继续读下去。
实现简单,将会使用到
Observer<Expr> 呢,这样我们可以同时支持按单个 Expr 求值的 REPL,以及按整个文件求值的直接执行。我们的任务是什么呢?
+ 支持
(fun () (println 1)) 和 (fun (a0 a1) (+ a0 a1)) 这样的 Abstraction 和 (f x y) 这样的 Application+ 支持 Lexical Scoping,当然也包括闭包这个东西
+ 支持 null Name 以及 Boolean Int Long 的字面表达,注意 Int Long 也包含二进制和十六进制表示法
稍后也会支持 Float Double Char String 的字面表达,以及
`() 这种 Scheme 式 symbol list。+ 支持 () 括号,<> 括号稍后会被我们用于语言扩展,以下我们会把它作为分词过程的语言构词,保留词。
此外,为了语言的一致性,我们要禁止对 [] 和 {} 的使用。
大家觉得,
(+ 1 (* 2 3)) 是怎么计算的?我们会把它折叠到 (+ 1 6) 然后继续算下去。每一个 (+)、(*) 都代表一颗『语法树』的子树,它们都有值,比如
1、(* 2 3) 这些都是子树啊。(+ expr expr) 这是它的类型,而 (+ 1 (* 2 3)) 的第一个 expr,是一个 literal "1",是子树。第二个是表达式,另一个子树。就是这样,那我们的语言怎么计算呢?或者说它怎么知道
(* 2 3) 是 6 呢?参考一下上面的全局作用域吧,我们在全局作用域提供
(* int int) 这个操作。我们可以把全局作用域做成一个 class,包含
get, set 方法interface GlobalScope: BasicMap<Symbol, Value> {
// val functions: BasicMap<Symbol, Value>
}
class Prelude: GlobalScope by mutableMapOf(
"*" to { a, b -> (a as Int) * (b as Int) }
)
这个语言认为它的 function 是 interface Invocable { operator fun invoke(vararg args: Value): Value }
这样,我们就能够提供对 (*) 计算的定义了——定义在 Kotlin 里面,一个基本操作。过会我们会通过简单的面向对象子类型多态 override 入门,认识 Visitor Pattern,写关于 evaluate, dump, check 这类操作的 AST Walker。
— 稍后的语言扩展 —
+ 支持 call-by-need,对参数进行惰性求值
现在我们是 call-by-value,在调用前对参数进行求值。
+ 可利用
(f x _) 进行的 Partial Application+
<1 + 2> 这样的中缀链,和 (infix l 4 +) (infix r 2 ->) 这种东西,不必支持特殊的词法规则待会我也会讲一下 Trie 树的实现,这样对可扩展关键字的实现,也可以自己思考下。