如何使用 JavaScript 实现一门编程语言(6) —— Interpreter

到目前为止,我们写了3个函数:InputStream,TokenStream 和 parse。为了从一段代码中获取AST,我们可以执行以下操作:

var  ast = parse (TokenStream (InputStream (code ))) ;

获取AST后就可以编写Interpreter(解释器)了,这比写parser容易。我们只需走AST,以正常顺序执行表达式。

执行环境

正确执行表达式的关键是正确维护执行环境 - 一个拥有绑定变量的上下文。它将作为参数传递给我们的evaluate函数。每次我们进入”lambda”节点时,我们都必须用新变量(函数的参数)扩展环境,并用运行时传递的值对它们进行初始化。如果一个参数影响了外部作用域的变量,我们必须小心地在离开函数时恢复先前的值。

实现这个最简单的方法是使用JavaScript的原型继承。当我们输入一个函数时,我们将创建一个新的环境。这种方式当我们退出时,我们不需要做任何事情——外部env已经包含在对象本身。

Read More

如何使用 JavaScript 实现一门编程语言(5) —— AST

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

之前我们讲过,parser将构建一个忠实地表示程序语义的数据结构。这里的AST节点则是一个普通的JavaScript对象,它具有一个type属性,用于指定它是什么类型的节点,还有一些附加信息,附加信息的值可能因 type 而异。

下面是我们语言所有 AST 节点列表:

Read More

如何使用 JavaScript 实现一门编程语言(4) —— Token stream

tokenizer(标记器, 也称为“词法分析器”), 对 字符输入流 进行操作,并返回具有相同接口的流对象,但由 peek()/next() 返回的值一个个token。token是具有两个属性的对象:type和value。以下是我们所支持的token的一些示例:

{  type:“ punc ”,value:“ (”  }            // 标点符号:parens,逗号,分号等等
{  type:“ num ”,value:5  }               // numbers
{  type:“ str ”,value:“ Hello World !“  }  // 字符串
{  type:” kw “,value:”lambda “  }         // keywords
{  type: ” var “, value: ” a “  }             // 标识符
{  type: ” op “, value: ” != “  }             // 运算符

空白符和注释将被跳过,没有令牌返回。

为了编写tokenizer,我们需要更仔细地认识我们的语言的语法。有个办法是,根据当前字符(由input.peek()返回的)来决定读取哪种类型的token:

  1. 首先,跳过空格。
  2. 如果然后返回。input.eof()null
  3. 如果它是一个井号(#),则跳过注释(在行结束后重试)。
  4. 如果它是一个引号,那么阅读一个字符串。
  5. 如果它是一个数字,那么我们继续阅读一个数字。
  6. 如果它是“字母”,则读取标识符或关键字token。
  7. 如果它是标点符号之一,则返回标点符号token。
  8. 如果它是运算符,则返回运算符token。
  9. 如果以上都不是,那就抛出错误了。input.croak()

“read_next”函数作为tokenizer的核心部分 ,它实现了上面的内容:

Read More

如何使用 JavaScript 实现一门编程语言(3) —— Input stream

这是最简单的部分。我们将创建一个“流对象”,它提供了从字符串中读取字符的操作。其中4个方法:

  • peek() - 返回下一个值,但不从流中移除它。
  • next() - 返回下一个值,并将其从流中丢弃。
  • eof() - 当且仅当流中没有更多值时才返回true。
  • croak(msg) - throw new Error(msg)

之所以包括最后一个,是因为流可以很容易地跟踪当前位置(即行/列),这对于显示错误消息很重要。

您可以根据您的需求随意添加更多的方法在流对象中,但对于我的教程这些就足够了。

因为流对象主要用来处理字符,所以 next()/ peek() 方法返回的值也是字符(JS没有char类型,它们是包含一个单一字符的字符串)。

Read More

如何使用 JavaScript 实现一门编程语言(2) —— 编写一个解析器

编写语言解析器是一项适度复杂的任务。实质上,它必须将一段代码(我们所看到的一堆字符)转换为“抽象语法树”(AST)。
AST是程序在内存中一种结构化的表达方式,它是“抽象”的,因为它不关心源代码是由哪些字符组成的,而是忠实地表示它的语义。我写了一个单独的页面来描述我们的AST。

例如,对于以下程序文本:

sum = lambda(a, b) {
  a + b;
};
print(sum(1, 2));

Read More

如何使用 JavaScript 实现一门编程语言(1) —— 前言

这是一系列关于 如何实现编程语言 的教程。如果你曾经写过一个解释器或编译器,那么这里可能没有什么新东西。但是,如果您使用正则表达式来“解析” 任何看起来像编程语言的东西,那么请至少阅读解析部分。让我们写出更少的错误代码!

目标受众是普通的 JavaScript / NodeJS 程序员。

我们要学什么?

  • 什么是解析器,以及如何编写解析器。
  • 如何编写解释器。
  • 为什么它们很重要。
  • 编写一个编译器。
  • 如何将代码转换为延续传递样式。
  • 一些基本的优化技术。

在两者之间,我会争论为什么 Lisp 是一种优秀的编程语言。 但是,我们将要使用的语言不是 Lisp。它有一个更丰富的语法(每个人都知道的经典中缀符号),除宏之外,它的功能与 Scheme相当。
可悲的是,宏是 Lisp 的最终堡垒,其他语言无法克服(除非它们被称为 Lisp 方言)。

首先,让我们想想我们的要实现的编程语言应该是什么样子。

我们应该想清楚自己想要实现的目标。把语法的严格描述放在一起是一个好主意,但是我会在本教程中使语法更加简单,下面的示例就是我们要实现 “λ” 语言:

# this is a comment

println("Hello World!");

println(2 + 3 * 4);

# functions are introduced with `lambda` or `λ`
fib = lambda (n) if n < 2 then n else fib(n - 1) + fib(n - 2);

println(fib(15));

print-range = λ(a, b)             # `λ` is synonym to `lambda`
                if a <= b then {  # `then` here is optional as you can see below
                  print(a);
                  if a + 1 <= b {
                    print(", ");
                    print-range(a + 1, b);
                  } else println("");        # newline
                };
print-range(1, 5);

请注意,标识符名称可以包含负号字符(print-range)。这是个人品味的问题:我总是在操作符旁边放置空格,我不喜欢太多的 camelCaseNames,而且我觉得短划线比下划线更好。编写自己的语言的好处是,你可以随心所欲地做到这一点。:)

输出是:

Hello World!
14
610
1, 2, 3, 4, 5

Read More

前端开发如何让持续集成/持续部署(CI/CD)跑起来

近几年,伴随着前端技术日新月异的发展,前端开发中前后端分离,工程化,自动化等现代化的开发模式越来普及,前端项目也引入了编译,构建,单元测试等现代软件工程化的标准环节。这样大提高了前端的开发效率和业务交付能力。但是,在代码集成,项目部署阶段,我们还需要引入 CI / CD 等现代化的软件开发实践,来减少风险,重复过程,节省我们的时间。

我们废话少说,这里不再对 持续集成/持续部署(CI/CD) 的概念做过多赘述,本文主要分享一下如何基于 gitlabjenkins 让 CI/CD 跑起来。

其中:

  • gitlab 用于代码版本管理,并通过其提供的 webhook 功能,触发 jenkins job 的运行。
  • jenkins 用来执行项目中 单元测试,编译打包相关 npm 命令,并发送反馈邮件,执行远程部署脚本。
  • nodejs 用于提供单元测试,编译打包功能的 npm 命令

在我们的前端项目里(如: https://github.com/hanan198501/vue-spa-template ),一般使用 karma 来运行单元测试,使用 webpack 来进行打包构建, 使用 npm script 来执行这些任务,其中我们的 package.json 的 scripts 字段就有如下:

"dev": "node build/dev-server.js",
"build": "node build/build.js", //打包构建
"build-server": "node build/build-server.js",
"unit": "karma start test/unit/karma.conf.js --single-run", // 运行单元测试

如果没有 CI/CD, 我们的前端从开发到提测工作流程可能如下:

  1. 本地机器上写代码
  2. 在命令行输入 npm run unit,查看单元测试结果
  3. 提交代码,push 到 git 远程仓库
  4. 登录测试服务器,拉取代码,执行 npm run build,构建项目
  5. 如果测试服务器是基于 pm2 的 proxy server,还需要重启 server

这个流程中,每一个步骤都要重复人工操作,很大增加了时间成本,不能保证操作的准确性。对于 unit 或者 build 的结果,没有一个自动的反馈机制,需要人工 check 运行结果,最后部署也是人工登录服务器执行脚本,非常繁琐。

引入 CI/CD 以后,整个流程变成:

  1. 本地机器上写代码
  2. 提交代码,push 到 git 远程仓库
  3. git hook 触发 jenkins 的构建 job (自动)
  4. jenkins job 中拉取项目代码,运行 npm run unit 和 npm run build,如果失败,发送邮件通知相关人。(自动)
  5. jenkins job 中执行测试服务器的部署脚本 (自动)

在 CI/CD 流程中,只有步骤1和步骤2需要人工操作,其他步骤都是自动运行,是一个非常标准化的流程,减少了人工操作的风险,省去了重复性工作,增强了项目的可见性。接下来我们将通过配置 jenkins 和 gitlab webhook 来实现这个流程。

Read More

基于 vue 全家桶的 spa 项目模板

项目简介

Github: https://github.com/annnhan/vue-spa-template

最近在给团队做前端技术改造,移动端方面主要使用 vue2.0 重构,这是基于 vue-cli 脚手架生成项目模板。我们做了一些改造,
加入了 vue-router ,vuex 等配套设施,本地 dev server 中加入了接口 mock 功能,还增加一个 build server 来预览 build 结果页面,前后端通过 spa 的方式实现分离,并相应做了分离后的联调,部署方案。
通过这个项目模板,可以快速搭建起用于前后端分离后的单页应用开发环境,项目主要包含:

  • 基础库: vue.jsvue-routervuexwhatwg-fetch
  • 编译/打包工具:webpackbabelnode-sass
  • 单元测试工具:karmamochasinon-chai
  • 本地服务器:express

Read More

使用 javascript 配置 nginx

在上个月的 nginx.conf 2015 大会上, 官方宣布已经支持通过 javascript 代码来配置 nginx,并把这个实现称命名为——nginscript。使用 nginscript,可以很轻易得在 niginx 配置文件中通过 js 语法来实现自定义的服务器配置。

安装

# 下载最新版本的 nginx 并解压
curl -O http://nginx.org/download/nginx-1.9.5.tar.gz
tar -xzvf nginx-1.9.5.tar.gz

# 下载 nginscript 模块并解压
curl -O http://hg.nginx.org/njs/archive/tip.tar.gz
tar -xzvf tip.tar.gz

# 编译并安装 nginx
$ cd nginx-1.9.5
$ ./configure --add-module=刚才解压的nginscript目录
$ make
$ make install

Read More

无梯子如何正常访问使用了 googleapis 的网站

许多国外的网站都使用被墙掉的 googleapis.com 上的静态资源(如 font、js库 等),如果不爬墙,这些资源无法加载,导致网站无法正常访问。

其实,360提供了一个国内的 googleapis.com 镜像仓库(libs.useso.com)。我想,当遇到 googleapis 的请求时,浏览器能自动跳转到360的镜像,就能正常访问了。

正好,我以前写过一个 chrome 插件 ReRes,可以通过它来完成这项工作。

安装好 ReRes 后,添加一条规则:

If URL match:^http\:\/\/(.+?)\.googleapis\.com\/(.+?)$
Response:http://$1.useso.com/$2

当浏览器中的请求匹配到 ^http\:\/\/(.+?).googleapis.com\/(.+?)$ 这个正则时,会自动跳转到 http://$1.useso.com/$2 这个 url ,其中 $1 和 $2 分别是正则中捕获到的两个分组。

如此,就可以正常访问使用了 googleapis 的网站。