About
RSS

Bit Focus


填坑 - Stekinscript 异步表达式

    深夜潜入一家竞争对手公司, 可惜只偷到了最后一页源代码的打印稿, 回来一看
            });
        });
    });
})();
    这是何等的引人泪下的故事.

    这不再是 Javascript 刚刚发明的那个年代, 程序员一边用 Java 一边咒骂着花括号嵌套又嵌套何时是个头; 有经验的程序员告诫那些乳臭未干的学生军函数一定要写短, 逻辑多拆分. 有时会想着, 有什么语言会强制规定单一函数的复杂度, 以让每个函数几乎都能限制在一二十行之内呢? 结果现在这个梦想成真了, 对于 Javascript 程序员而言, 这是不得不做的纯天然方式.
    比如 Javascript 中采用异步的方式来解决 IO 效率低的问题, 下面是一段用来读取 "/etc/passwd" 内容的 node 代码
fs: require('fs');
fs.readFile('/etc/passwd', function(err, data) {
    if (err) {
        return console.error(err);
    }
    console.log(data.toString());
});
    看着全局空间里洋洋洒洒一大串内容, 其实只有两句话, 第一句引入库, 第二句读文件; 而对文件的处理则单独放在另一个函数了. 多好的强制函数拆分机制, 现在再也不担心老大说函数写太长了. 不过稍微复杂一些的需求就开始疼了, 比如实现从 "/etc/passwd" 中读取前 32 个字节, node 代码为
fs: require('fs');
fs.open('/etc/passwd', 'r', function(err, fd) {
    if (err) {
        return console.error(err);
    }
    fs.read(fd, new Buffer(32), 0, 32, 0, function(err, bytes, buffer) {
        if (err) {
            fs.close(fd);
            return console.error(err);
        }
        console.log(buffer.toString());
        fs.close(fd);
    });
});
    望着这回调套回调向着行中陷落下去的代码, 感觉就是挖一个大坑. 各位 Javascript 程序员肯定不希望自己的宝贝女儿在幼儿园被问到「你父亲是做什么的」的时候回答「我也不太清楚但是看起来像是用在电脑里画楼梯的」吧.

    所以我想, 得从坑里爬出来.
    考虑到「相当一部分的异步调用貌似都是这个语句块的最后一个语句」继而有了「那为什么不在代码里面用标记让一个语句块里的语句实际上是两部分或更多部分, 第一个部分对应于当前语句块, 而后面的每个部分各是一个异步回调的函数体」这样的想法.
    有了这个想法后就打起了草稿, 比如上面的前一个例子, 要是写成这样
fs: require('fs')
fs.readFile('/etc/passwd', %(err, data))
if err
    return console.error(err)
console.log(data.toString())
    感觉如何? %(err, data) 这就是一个标记, 称之为异步占位符, 它可以作为函数调用的实参出现 (不能单独写在语句中或者进行算术运算). 异步占位符表示一个匿名函数 (参数列表同异步占位符中标识符列表), 如果一个语句中的某个函数调用中有实参为异步占位符, 那么同一个语句块内, 这个函数调用之后的部分都会被移进异步占位符所表示的匿名函数的函数体中去.
    也就是说, 上述代码中, 该匿名函数中的代码会包括
if err
    return console.error(err)
console.log(data.toString())

Permanent Link: /p/504 Load full text

Post tags:

 NodeJS
 Asynchronous Expression
 Stekin

句读探析 - Stekinscript 实现折行及多行 lambda 语法

上篇

自动机与表达式折行语法分析

概述

    在分析折行实现前, 先说说对基本的算术结构的分析.
    自动机基类 grammar::AutomationBase (grammar/automation-base.h: 30) 能够处理所有没有在 Yacc 语法规则中给出的细节, 比如将因子与算符组成算式, 分析括号配对, 还有对逗号, 冒号等分隔符的处理.
    自动机对可以处理的项目作了基本的分类, 对应于其中各名为 pushXxx 的函数和 matchClosing 函数. 而 matchClosing 函数相当于将 pushCloseParen, pushCloseBracket 等三种不同的结束括号合并成一个函数了, 因为大部分自动机自身不处理三种结束括号, 而将它们派发给其它的自动机处理.
    什么, 自动机还有很多种? 没错, 在 Stekinscript 语法模块设计中, 用来分析表达式的自动机有多种, 不同的自动机互相利用使得代码复杂度能够大大降低. 举个例子, 算术运算自动机 grammar::ArithAutomation (grammar/expr-automations.h: 9) 本身不处理大括号配对, 但表达式里面终归是可以出现大括号裹起来的字典的; 当这种情况发生时, 算术自动机会创建一个字典识别自动机 grammar::DictAutomation (grammar/expr-automations.h: 132), 让它顶替自己处理接下来的部分; 而这个字典自动机自己又是很懒的, 它才不会自己动手去识别用表达式描述的字典的键跟值, 怎么办呢? 与是它再新建一些算术自动机来识别表达式, 而自己只坐等逗号冒号等分隔符, 或者花括号结束.
    无论哪一种自动机, 都只会对特定的一些种类的 Token 感兴趣, 而基类 grammar::AutomationBase 对任何项目的实现都是一句话 (grammar/automation-base.cpp: 58-97)
error::unexpectedToken(posimage);
    那就是报错. 也就是说如果实现的自动机子类不重写它的函数, 就相当于子类丢弃了这个 Token 并报错. 除了这种粗暴的对待方式, 大致上来说每个函数还有如下方式对待 Token
  • 接受并记录, 比如 ArithAutomation 对象在正确的时候遇到运算符或者因子
  • 接受并丢弃, 这个说法有点语死早的感觉, 例子就是上面 ArithAutomationDictAutomation 的故事, 在 ArithAutomation 对象遇到开始的大括号时, 它会创建一个 DictAutomation 对象, 然后丢弃大括号 (留着也没用)
  • 将自身归约并移交, 比如刚才提到, DictAutomation 会委托 ArithAutomation 为它识别键值表达式, 那么此时如果 ArithAutomation 遇到一个冒号, 那么它自身不会处理, 而是首先将自己归约成一个表达式 grammar::Expression (grammar/node-base.h: 24) 对象, 并传递给它的委托者, 然后将这个冒号也一并移交给其委托者处理
  • 将自身归约并丢弃, 比如 DictAutomation 遇到结束的大括号时, 它会将自己归约成一个字典 grammar::Dictionary (grammar/expr-nodes.h: 228) 对象, 完成使命, 然后弃掉结束大括号

自动机栈

Permanent Link: /p/501 Load full text

Post tags:

 Syntax Analysis
 Stekin
 Compiler Construction

NodeJS liteview Postgres Stekinscript 搭建留言板 [完]

上篇

前期准备

安装软件与库

    需要安装
  • Postgres (建议 9 或以上版本)
  • libpq-dev
    npm 安装 Postgres 库
npm install pg

配置数据库与表

    参考这篇文章前半部分配置 Postgres 数据库.
    如果已经为 PG 用户 psuser 授权使用 mydb 这个数据库, 现在进入数据库
psql -Upsuser -dmydb -W
Password for user psuser:
建立数据表用于存放留言
mydb=> CREATE TABLE message (
mydb(>   id SERIAL,
mydb(>   content TEXT,
mydb(>   post_time TIMESTAMP DEFAULT NOW(),
mydb(>   PRIMARY KEY(id)
mydb(> );
NOTICE:  CREATE TABLE will create implicit sequence "message_id_seq" for serial column "message.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "message_pkey" for table "message"
CREATE TABLE

显示留言信息

更新页面模板

    之前的页面模板实在显得太单薄, 下面修改一下准备放入留言数据
<html>
<head><title>Guestbook</title></head>
<body>
#{foreach(#messages)}
<div>
    <p style='font-size: 0.6em'>#{[post_time]}</p>
    <p style='margin-left: 20px'>#{[content]}</p>
</div>
#{end}
    liteview 文档如是说
  • #{foreach(#XXX)} 即在 XXX 对象上作循环
  • #{[YYY]} 即每次循环的循环变量的 YYY 属性
    因此上面的页面模板需要传入留言对象列表, 每个留言对象有属性 post_time, content. 这命名与数据库表是一致的.
    然后改一下 index.stkn, 放假数据进去先看看效果
view: require('liteview').create()

exports.get: (request, response):
    response.writeHead(200, { 'Content-Type':: 'text/html' })
    response.end(view.render('index.html', {messages: [
        {
            post_time: '1970-01-01 00:00:00',
            content: 'Nostalgia',
        },
        {
            post_time: '1970-01-01 00:00:00',
            content: 'Eternal Rite',
        },
        {
            post_time: '1970-01-01 00:00:00',
            content: 'Shinto Shrine',
        },
    ]}))
    刷下页面, 这些东西就出现了.

从数据库加载内容

    巧妇难为无米之炊, 现在首先要做的是向数据库插入一些数据. 还是更刚才建表一样, 进入 psql, 使用下面的语句插入一些水帖

Permanent Link: /p/500 Load full text

Post tags:

 Postgres
 Stekin
 Tutorial
 Web Server
 NodeJS

NodeJS liteview Postgres Stekinscript 搭建留言板 [零]

前期准备

配置软件与库

    需要安装
  • nodejs 0.8 或以上版本 (之后编译 Postgres 模块需要高版本)
  • npm
  • 下载编译 Stekinscript (非必需) $ git clone git://github.com/neuront/stekinscript.git
    Ubuntu 用户安装 nodejs 0.8 版本请加入下面的源
add-apt-repository ppa:chris-lea/node.js
    创建一个工作目录, 通过 npm 安装要用到的模块
npm install liteview
npm install validator

Stekinscript 快速入门 (非必需)

    请参见 https://github.com/neuront/stekinscript/wiki/Language-Specification.
    对 Stekinscript 没有兴趣的同学可以无视这一步, 在每节之后生成的 Javascript 代码会奉上, 对于 Stekinscript 代码, 可以认为它是更易阅读的 JS.

开工

上手

    创建如下内容的 main.stkn 文件, 先启动一个简单的服务器
http: require('http')

port: 8888

http.createServer((request, response):
    response.writeHead(200, { 'Content-Type':: 'text/html' })
    response.end('<html><body>Hello, World!')
).listen(port, '127.0.0.1')

console.log('Server running at local host port=' + port)
    使用 stekin 编译它 (请用合适的路径替换下面的 stekin, 或者将编译生成的 stekin 可执行程序加入 PATH)
stekin -i require < main.stkn > main.js
    生成的 JS 代码类似 (下面这是经过去除额外括号与重新排版的, 实际上现在 Stekinscript 生成的代码真是惨不忍睹)
(function () {
    const s_http = require("http");
    s_http.createServer(function (s_request, s_response) {
        s_response.writeHead(200, {
            "Content-Type": "text/html"
        });
        s_response.end("<html><body>Hello, World!");
    }).listen(8888, "127.0.0.1");
    console.log("Server running at local host port=8888");
})();
    然后使用 node 来运行它, 就在本地搭建了一枚服务器
node main.js
Server running at local host port=8888
    访问 http://localhost:8888/ 就可以看到服务器响应了.

Makefile

Permanent Link: /p/499 Load full text

Post tags:

 NodeJS
 Stekin
 Web Server
 Tutorial

句读探析 - Stekinscript 对缩进语法的实现

获取源代码

    Stekinscript 的项目地址在 https://github.com/neuront/stekinscript, 获取最新版代码
git clone git://github.com/neuront/stekinscript.git
    本文将以 2012 年 11 月 15 日签入的版本介绍如何设计编译器语法部分, 如果 clone 的版本不是此版本, 可以通过下面的指令回滚到该版本
$ cd stekinscript
$ git reset --hard 9b756ad111fa3cf1d78a89e050a1adf38fa5e0b6

外围模块概述

    下面内容中, 凡是首次提到某个类或函数时, 会在括弧中用文件路径: 行号的方式给出它所声明或定义的位置, 如 main 函数 (stekin.cpp: 11); 上下文很明显有说明哪个文件时, 以 L 行号 说明在哪一行.

对 C++11 的使用

    既然是用 C++ 实现那么内存管理总是个大问题, Stekinscript 自然也要为此做好准备.
    Stekinscript 的内存管理大量依赖于 C++11 中的 std::unique_ptr 跟 move semantic, 并且对 std::unique_ptr 自有一套封装, 包括如下的几个类型
  • util::sptr (util/pointer.h: 94) 是直接对 std::unique_ptr 的封装, 但不允许通过 get() 方法直接取得其中的裸指针, 也不允许通过单目 * 操作符获取对象的引用, 单目型号操作符获取的是 util::sref 类型对象
  • util::sref (util/pointer.h: 23) 是共享引用类型, 几乎等价于一般对象引用类型, 只是不可以通过单目 & 操作符取引用, 另外使用 -> 操作符使用其内部的对象而不是直接使用 . 操作符
    以上类型一般只用于存在多态的情况.
    C++11 的基础知识可以参考这些文章.

错误处理

    需要安装有 Python 2.7 版本, 在源码目录下执行
make code-gen
    生成 report/errors.h 与 report/errors.cpp 文件 (一同生成的还有单元测试需要的 test/phony-errors.h test/phony-errors.cpp).
    report/errors.h 包括了所有编译报错函数, 凡是源代码中出现 error:: 名字空间中的调用均声明于此. (虽然生成的代码格式上寒碜了点, 勉强可读)

Lex/Yacc

    既然是讲 C++ 程序, 本该从 main 函数入手, 但 Stekinscript 有一些依赖在初始化环境 (stekin::initEnv env.h: 9) 之后, 立即调用了 Yacc 中的 yyparse() 函数, 所以这时要去看 Lex/Yacc 文件了.
    grammar/lexica.l 文件定义了词法单元, 其中行首空格 (L 19) 不可被忽略 (否则哪来的缩进语法), 而行首不能有制表符否则报错 (L 24). 缩进层次将被缓存在全局变量 grammar::last_indent 中 (grammar/yy-misc.h: 23). 其它的词法单元看看便罢.Analysis
    然后小的重头在 grammar/syntacticall.y 这个文件中, 它实现了部分简单的语法规则. 去掉其中的语句, 开头的一些产生式如下 (L 42)

Permanent Link: /p/498 Load full text

Post tags:

 Stekin
 Compiler Construction
 Syntax Analysis

句读探析 - 又一个 Javascript 生成器

    前段时间博客没怎么更新, 因为不才去刷一个编译器项目了. 这项目是个 Javascript 产生器.
    起这个念头的契机是看到连 M$ 都动心思搞 JS 生成器了 (然而不得不吐槽的是, 这语言看起来跟 JS 简直一个德行). 而模仿的对象则是 CoffeeScript, 可是我又不想去分支 CoffeeScript 改点语法什么的改成 LatteScript 或 CappuccinoScript (我对 Coffee 的某些语法不很满意), 于是乎自己折腾了一个.
    不过另一方面, 这个编译器 (暂定名是 Stekinscript) 也不是完全无中生有, 而是从我的毕业项目修改过来的. 要说这样有什么不好的话, 就是 --- 这东西用 C++ 实现的 (准确地说是 C++11); 要说有什么好的话, 就是貌似这样可以直接集成 v8, 嗯 (自黑一下, 我没考察过这个, 也没有做这个事情的计划).

    下面介绍一下这个语言本身, 撇开日常的什么分支语句啦函数定义返回这些 (详情可参考文档), 先说说重点.
    首先 Stekinscript 支持缩进语法, 类似 Python 跟 CoffeeScript. 代码很重要的一点就是要看着舒服不是么?
    其次就是非强制类型, 强制类型给人的感觉呢, 就像三岁小孩子学写字旁边有个执戒尺的先生一样, 错一点就打手, 我们已经是断奶的程序员了好么, 所以什么 Dart 就先搁下不用了.
    最后是实际使用的感觉, 这一点我排斥 CoffeeScript 的 lambda 语法如 (x)-> x * x 这种. 要知道输入类似 -> 这样的符号两个字符都在右手无名指跟小指区, 还是一上一下, 而且减号不用按住 Shift 而大于号又需要, 这是有多别扭设计语言的人没知觉么? 还是说已经练成星际争霸九段这点微操不算什么?

    吐槽就到此为止, 下面贴个 Stekinscript 的例子
x: []
setTimeout(():
    if x.length = 10
        return
    x.push(document.getElementById('input').value)
, 2000)

Permanent Link: /p/497 Load full text

Post tags:

 Compiler Construction
 Stekin
 C++

Pipeline: 我所希望的列表处理方法

    本文仅表现个人从事程序设计以来的感受和想法, 带有主观色彩, 可以认为结论式的句子均省略了 "我认为" 这样的字眼.

    编程语言中最基本的事情就是数据变换, 比如基本的四则运算, 或是求正弦, 或是将字符串拼接起来, 前面这三种都是针对数据个体的, 而且在不同语言中实现得大同小异. 而对于数据集体的操作, 比如对一个列表进行变换, 各种编程语言会有各自独特的方法, 甚至同一种语言在语言特性升级前后都会有不同, 比如在 C++ 中, 一直以来采用 for 来迭代, 在 C++11 新标准引入了 lambda 之后, std::for_each 就咸鱼翻身变得流行起来; 又比如在 Python 中, 列表迭代采用 for element in list 的方式, 而在引入 yield 之后的版本中, 转为了 for element in generator. 虽然语法没有变, 但是整体思想改变了不少.

    而在这些眼花缭乱迭代方式当中, 程序员究竟想要得到什么? 解决实际问题的过程中, 最常见的操作应当是
  • 映射: 给出一个函数 f 和集合 {x}, 得到新的集合 {f(x)}
  • 过滤: 给出一个函数 g 和集合 {x}, 得到新的集合 {x 当且仅当 g(x) 为真}

    将这些需求提出来之后, 最让我感到有趣的莫过于 Shell 的管道. 比如这样一句
find -type f | awk '{ print "mv", $1, "/tmp/" }'
相当与进行了一次映射, 集合为 find -type f 给出的文件列表, 函数 f 就是 awk 程序. 而这个例子
find -type f | grep wav$
则是一个过滤.
    上面的例子中, 并没有任何迭代的过程, 可以说 Shell 的管道完全抽象了集合迭代, 让对集合的操作变为了对元素个体的操作. 在语言层面上将迭代过程本身封装起来, 这确实是一件很实在的事情. 因此, 我很乐意将这个特性引进到了 Stekin 程序设计语言中.

    在当前的 dev 分支上, Stekin 实现了类似管道的列表操作方式. 具体的语法类似
  • 列表 | return 表达式
  • 列表 | if 布尔表达式
分别表示映射和过滤.
    在 awk 中, 使用 $1 等表示该行的第一个词, 而 $2 表示第二个, 等等. 而在 Stekin 中并没有这个困扰, 因为迭代的列表中元素类型是确定的, 因此直接使用 $element 来表示元素, 如下面的代码
list: [0, 1, 2, 3, 4]
sqr_list: list | return $element * $element
将每一个元素 $element 映射到其平方 $element * $element, 得到的 sqr_list 将是 [0, 1, 4, 9, 16].
    除了在管道中可以使用 $element 引用元素之外, 还可以使用 $index 引用当前元素的索引 (从 0 开始). 不过这种表示法仍然有待商榷, 我曾想过使用如 $$, $@ 这样看起来很 Shell 很 Makefile 的符号, 但那样实在太不直观, 于是使用了 $element 这样直接写单词的方法. 如果你有什么想法或建议, 欢迎在回复中提出来.

    而过滤操作则除了将管道符号 | 之后的 return 关键字换成 if 之外, 它要求接下来的表达式是布尔类型的. 当且仅当这个表达式的值为真时, 则该元素将被添加到结果列表中去, 如
list: [0, 1, 2, 3, 4]
new_list: list | if $element % 3 != 0

Permanent Link: /p/408 Load full text

Post tags:

 Stekin
 List Processing
 Pipeline


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site