/
2022-01-18-wenyan_stdin.md
279 lines (234 loc) · 15.5 KB
/
2022-01-18-wenyan_stdin.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
---
date: 2022-01-18T17:04:31+0800
last_modified_at: 2023-05-31T21:16:21+0800
tags: 编程 esolang
---
# “文言”编程语言能读取标准输入了
前不久我给[“文言”][wenyan]写了个扩展库,让它能够通过 Node.js 读取标准输入。现已被收入“文言”的包管理系统[“文淵閣”][wyg]。
研究过这门深奥编程语言的朋友可能知道,“文言”中没有原生的办法来读取标准输入。我猜测这大概也是[洛谷网][luogu]不再支持这门语言的原因之一。
[wenyan]: https://wy-lang.org/
[luogu]: https://www.luogu.com.cn/
[wyg]: https://wyg.wy-lang.org/
<aside class="card my-3 p-3 pb-0">
<figure>
<blockquote>
<p><b lang=en>Esoteric programming language</b>,简称<span>Esolang</span>,它们的设计被用于测试计算机语言设计的极限,作为一个概念的证明,或仅仅是一个玩笑。<span lang=en>Esolang</span>创作者[……]几乎不会在意语言的可用性,甚至恰恰相反,会故意增加使用难度。</p>
</blockquote>
<figcaption>
<cite>—— 张凯强<a href="https://cloud.tencent.com/developer/article/1560964">《文言文编程火了,可我完全学不懂》</a></cite>
</figcaption>
</figure>
</aside>
“文言”被洛谷网移除前,不乏有人用这门语言来解算法题,这就不得不用到嵌入 JavaScript 代码的 hack。由于“文言”代码需要先编译成 JavaScript代码才能运行,而编译器没有很严格地检查代码是否符合正常语法,我们可以轻松地注入 JavaScript 表达式:
```wenyan
施「(str=>process.stdout.write(str))」於『問天地好在』。
```
{: lzh}
在这句代码中直接嵌入了一个 JavaScript 箭头函数 `str => process.stdout.write(str)`{: js}。利用这种方法,我们可以调用 Node.js 环境下的标准库来实现读取输入。但问题是,如果直接读取 `/dev/stdin` 的内容,就必须一次读取完整个输入数据,而无法在命令行进行人机交互。
如果是 Node.js 开发,一般会采用原生的 `readline` 模块来读取用户输入。但它是异步运行的,要想用它来给“文言”程序读取输入,大概需要修改整个“文言”编译器的代码,让它编译出支持异步的程序,这似乎不太现实。于是我通过查阅各种资料,摸索出来了不使用异步操作读取命令行输入的办法:
```js
// gets.js
const fs = require("fs")
const SEGMENT_LEN = 1024
const EOL_BUFFER = Buffer.from(require("os").EOL)
function gets() {
// 缓冲区,以及已读入的字节数
let buffer = Buffer.alloc(SEGMENT_LEN)
let len = 0
while (true) {
// 读取一字节,如果 EOF 就停止读入
if (fs.readSync(0, buffer, len, 1) === 0) break
++len
// 如果已经换行就停止读入
if (buffer.subarray(len - EOL_BUFFER.length, len).equals(EOL_BUFFER)) break
// 如果缓冲区已经写满就扩容
if (len === buffer.length) {
const oldBuffer = buffer
buffer = Buffer.alloc(oldBuffer.length + SEGMENT_LEN)
buffer.set(oldBuffer)
}
}
return buffer.subarray(0, len).toString()
}
```
这里声明了一个 `gets()`{: js} 函数,可以读取一行用户输入。方法稍显笨拙:每次用 `fs.readSync(0)`{: js} 读取一个字节,这里如果用户还没有输入完成并按下回车,就会阻塞程序,等待用户输入;用户按下回车后,程序就会逐字节地读取输入的内容,直到遇到换行符为止。
<aside markdown='block' class="card my-3 p-3 pb-0">
注意我传给 `fs.readSync()`{: js} 的第一个参数 `0`{: js} 实际上是标准输入的文件描述符 `process.stdin.fd`{: js},由于这样写有时会出问题就改成了直接的 `0`{: js}。
</aside>
原型有了,就可以用“文言”来实现了。我定义了一个 `「閱行」`{: wy} 函数,并给它创建了语法糖 `閱一行`{: wy}、`閱二行`{: wy}、`閱三行`{: wy} ……一直到 `閱九行`{: wy},分别对应调用函数 `「閱行」`{: wy} 一次至九次。这样一来,我们就可以很方便地连续读取多行输入:
```wenyan
閱三行。名之曰「甲」曰「乙」曰「丙」。
```
{: lzh}
这就相当于:
```wenyan
施「閱行」。施「閱行」。施「閱行」。名之曰「甲」曰「乙」曰「丙」。
```
{: lzh}
不过,在解算法题的时候,我们往往需要从输入中读入数字、字符、单词,而不是读取一整行,所以我还添加了这些读取特定类型数据的方法:`「閱數」`{: wy}、`「閱字」`{: wy}、`「閱言」`{: wy},并为它们定义了相应的语法糖。
这样一来,用“文言”来解 [A+B Problem][aplusb] 就可以这样写:
```wenyan
閱二數。名之曰「甲」曰「乙」。
加「甲」以「乙」。書之。
```
{: lzh}
当然,要想得到正确的输出,我们不能直接用“文言”的解释器来运行这个程序,因为这样会输出中文数字;需要先把程序编译成 JavaScript,再调用 Node.js 运行编译出的代码。
```shell
wenyan -c program.wy > compiled.js
node compiled.js < input.txt > output.txt
```
在“文言”编程中,输出被称作“**書**{: lzh}”,那么读取输入不妨叫做 “**閱**{: lzh}”;许多第三方的扩展库都叫做“某某**秘術**{: lzh}”,因此我决定把我的这个库命名为“**閱文秘術**{: lzh}”。
[这里][src]是“**閱文秘術**{: lzh}”的 GitHub 仓库。
[aplusb]: https://www.luogu.com.cn/problem/P1001
[src]: https://github.com/DGCK81LNN/wenyan-stdin
{:lzh: lang='lzh-Hant'}
{:wy: .highlight.language-wenyan lzh}
{:js: .highlight.language-javascript}
<aside class="accordion my-3">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type='button'
data-bs-toggle='collapse' data-bs-target='#collapse1'
aria-expanded='false' aria-controls='collapse1'
>图片</button>
</h2>
<div class="accordion-collapse collapse" id='collapse1'>
<div class="accordion-body pb-0" markdown='block'>
![“閱文秘術”的代码中用到了很多嵌入的 JavaScript 表达式,它们跟“文言”代码的古汉语结合在一起,整体看起来十分怪异。]({%link assets/2022-01-18-1.jpg %})
{: style="max-width: 500px; display: block; margin: auto;"}
![我节选了“閱文秘術”的一部分代码发给朋友,他形象地称嵌入的 JavaScript 表达式为“来自西洋巫术的神秘咒语”。]({%link assets/2022-01-18-2.jpg %})
{: style="max-width: 500px; display: block; margin: auto;"}
</div></div></div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type='button'
data-bs-toggle='collapse' data-bs-target='#collapse2'
aria-expanded='false' aria-controls='collapse2'
>“閱文秘術”的完整源代码,带语法高亮</button>
</h2>
<div class="accordion-collapse collapse" id='collapse2'>
<div class="accordion-body pb-0" markdown='block'>
```wenyan
夫「require("fs")」。名之曰「fs」。
吾有一物。曰「Buffer.from(require("os").EOL)」。名之曰「行尾」。
吾有一言。名之曰「文」。
吾有一爻。名之曰「載畢」。
吾有一術。名之曰「載文」。是術曰。
若「載畢」者。乃歸空無。
或若「文」之長不等於零者。
注曰。『既載之文尚未閱畢。不消更載。』
乃歸空無也。
吾有一物。曰「Buffer.alloc(0)」。名之曰「器」。
吾有一數。名之曰「器容」。
恆為是。
若「器容」等於「器」之長者。
夫「器」。名之曰「舊器」。
夫「舊器」之長。加其以一千零二十四。施「Buffer.alloc」於其。昔之「器」者。今其是矣。
施「(n=>o=>n.set(o))」於「器」於「舊器」。
昔之「舊器」者。今「null」是矣。
云云。
施「(sup=>sub=>len=>sup.subarray(len-sub.length,len).equals(sub))」於「器」於「行尾」於「器容」。
若其然者乃止也。
施「(buf=>len=>fs.readSync(0,buf,len,1))」於「器」於「器容」。
若其等於零者。
昔之「載畢」者。今陽是矣。
乃止也。
加「器容」以一。昔之「器容」者。今其是矣。
云云。
若「器容」不等於零者。
施「(buf=>len=>buf.subarray(0,len).toString())」於「器」於「器容」。加其於「文」。昔之「文」者。今其是矣。
云云。
是謂「載文」之術也。
今有一術。名之曰「閱畢乎」。是術曰。
若「載畢」者。若「文」之長等於零者。乃得陽也云云。
乃得陰。
是謂「閱畢乎」之術也。
或云『若已閱畢者』。蓋謂『施「閱畢乎」。若其然者』。
或云『若未閱畢者』。蓋謂『施「閱畢乎」。若其不然者』。
今有一術。名之曰「閱行」。是術曰。
施「載文」。噫。若已閱畢者乃歸空無也。
夫「文」。名之曰「言」。
昔之「文」者。今『』是矣。
乃得「言」。
是謂「閱行」之術也。
或云『閱一行』。蓋謂『施「閱行」』。
或云『閱二行』。蓋謂『施「閱行」。施「閱行」』。
或云『閱三行』。蓋謂『施「閱行」。施「閱行」。施「閱行」』。
或云『閱四行』。蓋謂『施「閱行」。施「閱行」。施「閱行」。施「閱行」』。
或云『閱五行』。蓋謂『施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」』。
或云『閱六行』。蓋謂『施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」』。
或云『閱七行』。蓋謂『施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」』。
或云『閱八行』。蓋謂『施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」』。
或云『閱九行』。蓋謂『施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」。施「閱行」』。
今有一術。名之曰「閱字」。是術曰。
施「載文」。噫。若已閱畢者乃歸空無也。
夫「文」之一。名之曰「字」。
夫「文」之其餘。昔之「文」者。今其是矣。
乃得「字」。
是謂「閱字」之術也。
或云『閱一字』。蓋謂『施「閱字」』。
或云『閱二字』。蓋謂『施「閱字」。施「閱字」』。
或云『閱三字』。蓋謂『施「閱字」。施「閱字」。施「閱字」』。
或云『閱四字』。蓋謂『施「閱字」。施「閱字」。施「閱字」。施「閱字」』。
或云『閱五字』。蓋謂『施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」』。
或云『閱六字』。蓋謂『施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」』。
或云『閱七字』。蓋謂『施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」』。
或云『閱八字』。蓋謂『施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」』。
或云『閱九字』。蓋謂『施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」。施「閱字」』。
吾有一術。名之曰「閱物」。欲行是術。必先得一物。曰「識物譜」。乃行是術曰。
施「載文」。噫。若已閱畢者乃歸空無也。
施「(s=>r=>s.match(r))」於「文」於「識物譜」。名之曰「識」。
夫「識」。若其不然者。乃歸空無也。
夫「識」之一。名之曰「言」。
夫「文」。夫「言」之長。取二以施「(s=>start=>s.slice(start))」。昔之「文」者。今其是矣。
乃得「言」。
是謂「閱物」之術也。
今有一術。名之曰「閱白」。是術曰。
吾有一言。名之曰「言」。
恆為是。
施「載文」。噫。
施「閱物」於「/^\s+/」。加其於「言」。昔之「言」者。今其是矣。
若已閱畢者乃止也。
若「文」之長不等於零者。乃止也。
云云。
乃得「言」。
是謂「閱白」之術也。
或云『閱一白』。蓋謂『施「閱白」』。
今有一術。名之曰「閱言」。是術曰。
閱一白。加其以「文」。名之曰「原文」。
若已閱畢者。昔之「文」者。今「原文」是矣。乃歸空無也。
施「閱物」於「/^\s*(\S+)/」。名之曰「言」。
若「言」者。施「(s=>s.trim())」於「言」。乃得矣。
若非。昔之「文」者。今「原文」是矣。
云云。
是謂「閱言」之術也。
或云『閱一言』。蓋謂『施「閱言」』。
或云『閱二言』。蓋謂『施「閱言」。施「閱言」』。
或云『閱三言』。蓋謂『施「閱言」。施「閱言」。施「閱言」』。
或云『閱四言』。蓋謂『施「閱言」。施「閱言」。施「閱言」。施「閱言」』。
或云『閱五言』。蓋謂『施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」』。
或云『閱六言』。蓋謂『施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」』。
或云『閱七言』。蓋謂『施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」』。
或云『閱八言』。蓋謂『施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」』。
或云『閱九言』。蓋謂『施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」。施「閱言」』。
今有一術。名之曰「閱數」。是術曰。
閱一白。加其以「文」。名之曰「原文」。
若已閱畢者。昔之「文」者。今「原文」是矣。乃歸空無也。
施「閱物」於「/^\s*-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[Ee][+-]?\d+)?/」。名之曰「言」。
若「言」者。施「parseFloat」於「言」。乃得矣。
若非。昔之「文」者。今「原文」是矣。
云云。
是謂「閱數」之術也。
或云『閱一數』。蓋謂『施「閱數」』。
或云『閱二數』。蓋謂『施「閱數」。施「閱數」』。
或云『閱三數』。蓋謂『施「閱數」。施「閱數」。施「閱數」』。
或云『閱四數』。蓋謂『施「閱數」。施「閱數」。施「閱數」。施「閱數」』。
或云『閱五數』。蓋謂『施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」』。
或云『閱六數』。蓋謂『施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」』。
或云『閱七數』。蓋謂『施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」』。
或云『閱八數』。蓋謂『施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」』。
或云『閱九數』。蓋謂『施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」。施「閱數」』。
```
{: lzh}
</div></div></div>
</aside>