hazeljs
用自定义行为扩展普通JavaScript程序的Hazel编辑框。
你可以带一个框,一个文本区域或者你自己的编辑器;Hazeljs对输入的内容进行类型检查,指出哪里出错了,在哪里出错了,然后运行它,即使它还没有完成。
淡褐色的(带有漏洞的类型化函数式编程语言)作为JavaScript库。从Node或web页面分析和运行Hazel程序,并调用它们在JavaScript中定义的函数。
const Hazel = require("hazeljs");
const double = Hazel.run("fun (x : Int) -> x * 2").value;
double(21); // 42
实验。 这是一个原型(0.0)。x): API可能在任何版本中更改。它是从草稿库中构建的 hazelgrove /榛# 2711 和 # 2719,在提交时
0d66c36618(Hazel.build的名字)。
为什么
Hazel类型检查并运行未完成的程序:有漏洞或类型错误的程序仍然运行,绕过它们。这使得它成为一种很好的代码语言,一个页面允许人们在运行时编写和修改:规则、脚本、练习。
我们调用一个JavaScript程序,其事件处理程序是Hazel函数 Hazlet:页面对每个事件调用Hazel函数,并对其返回的内容进行操作。第一个是游戏,下面下面 原型用例。
安装
npm install hazeljs
或者在web页面中,从提供npm包的CDN:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/hazel-lib.js"></script>
<script>
console.log(Hazel.run("1 + 2").value); // 3
</script>
该脚本定义了一个全局变量 Hazel (window.Hazel). 相同的文件在 https://unpkg.com/[email protected]/hazel-lib.js。
用法
CommonJS:
const Hazel = require("hazeljs");
ES模块,整个对象或其部分:
import Hazel from "hazeljs";
import { analyze, run, analyzeProgram, runProgram } from "hazeljs";
TypeScript类型包括(index.d.ts).
分析并运行
Hazel.analyze("1 + true");
// { errors: ["Error in term:\n true\nNature of error: Expecting type Int but got inconsistent type Bool"] }
Hazel.run("let x = 2 in x * 21");
// { errors: [], value: 42 }
程序的错误不会使其停止运行:Hazel会对其漏洞和错误进行评估,因此 value 是否存在这两种情况,然后代码决定接受哪些错误。
从JavaScript调用Hazel函数
分析一个程序一次,然后随心所欲地调用它的函数:
const rule = Hazel.run(`
type Cmd = + Done + Flip(Int, Cmd) in
type Event = + Pull(Int) in
fun e : Event -> case e | Pull(l) => Flip(l, Done) end`).value;
rule({ tag: "Pull", value: 2 }); // { tag: "Flip", value: [2, { tag: "Done" }] }
API
| 呼叫 | 返回 |
|---|---|
Hazel.analyze(text) | { errors }:程序的错误消息,以字符串的形式。 |
Hazel.run(text) | { errors, value }:与程序的值相同。 |
Hazel.analyzeProgram(spec) | { ok, errors, holes, programHoles, incomplete, programIncomplete, ms },用于命名模块程序(如下)。 |
Hazel.runProgram(spec) | 一样的,而且 value。 |
Hazel.version | 图书馆自己的标签,如 "0.1 (draft, js-library)"。 |
Hazel.build | 这个构建来自hazelgrove/hazel提交。 |
值
值可以以两种方式转换:Hazel的结果以这样的形式返回,而传递给Hazel函数的JavaScript值以这样的形式返回。
| 淡褐色的 | JavaScript |
|---|---|
Int, Float 还有其他数字 | 数量 |
Bool | 布尔 |
String | 字符串 |
| 一个列表 | 一个数组 |
() | null |
标记元组 (x = 1, y = 2) | 一个对象 {x: 1, y: 2} |
另一个元组 (1, true) | 一个数组 [1, true] |
一个构造函数 A、应用 B(3) | {tag: "A"}, {tag: "B", value: 3} |
| 一个函数 | 一个JavaScript函数(一个参数,或多个元组参数) |
其他任何东西(类型、洞)都会返回为 {hazel: "<its printing>"}。
JavaScript中的构造函数没有自己的类型,所以Hazel在参数被注释的地方检查它(fun e : Event -> ...). 对应用的构造函数进行整体、标签和有效负载检查;不适合该类型的变量将停留在参数上,调用返回 {hazel: ...}。
模块程序
对于每个模块都有一个编辑器的页面,将文本交给Hazel并让它从中构建一个程序:
const result = Hazel.runProgram({
preamble: "type Cmd = + Done + Say(String) in", // definitions every module sees, ending in `in`
modules: [
{ name: "Common", text: "let greet = fun (s : String) -> Say(s)" },
{ name: "Room", text: "let on_event = fun (n : Int) -> Common.greet(\"hi\")" },
],
tail: "Room.on_event", // what the program gives; () if left out
});
result.ok; // true: no errors, no holes, nothing left unfinished
result.value(1); // { tag: "Say", value: "hi" }
每个模块看到序言和它之前的模块,如 Common.greet尾巴能看见它们。模块的文本是它的主体:它的成员,没有 in。
errors:[{ module, message, id }],在那里module是错误所在模块的名称,还是null开头部分和结尾部分。holes:每个模块的孔,按名称({ Common: 0, Room: 1 });programHoles开头部分和结尾部分incomplete:每个模块的括号和关键字都是打开的(a(没有),一个case没有end)或关闭时没有开瓶器;programIncomplete开头部分和结尾部分ok:没有错误,没有漏洞,没有不完整。仍然在编写的函数可能有漏洞,但没有错误,所以要采取行动value只有当ok。ms表示通话所用时间,单位为毫秒。在0.0.1到0.0.3中,它读取0(一个已知的错误);自己计算打电话的时间。
Hazel记得它读过的最后一个程序的文本,一部分一部分地,所以在一个模块改变后再次调用只读取该模块。
错误、漏洞和未完成的代码
Hazel在运行程序之前读取整个程序,并将错误作为数据报告,而不是作为异常报告。一个来自《乱糟糟的装扮》陷阱房间的例子:杠杆是一个 Side, L 或 R房间规则传递的是一个数字。
const result = Hazel.analyzeProgram({
preamble: "type Side = + L + R in",
modules: [
{ name: "Common", text: "let flip = fun (s : Side) -> s" },
{ name: "TrapRoom", text: "let answer = Common.flip(3)" },
],
tail: "TrapRoom.answer",
});
{
ok: false,
errors: [{
module: "TrapRoom",
message: "Error in term:\n 3\nNature of error: Expecting type Side but got inconsistent type Int",
id: "3d0f0000-0000-4000-b53b-3f45d4c660e0",
}],
holes: { Common: 0, TrapRoom: 0 }, programHoles: 0,
incomplete: { Common: 0, TrapRoom: 0 }, programIncomplete: 0,
// and ms
}
程序未准备就绪有三种方式,每种方式都在其所在位置计算:
| 什么 | 例子 | 在哪里显示 |
|---|---|---|
| 一个错误 | Common.flip(3)一个数,其中 Side 是想要的 | errors,每个都有它的模块 |
| 一个洞:还没有写的部分 | Common.flip(?) | holes: { TrapRoom: 1 },没有错误 |
| 一些未完成的东西:一个括号或关键字没有它的伙伴 | Common.flip(L | incomplete: { TrapRoom: 1 } |
一个只有文字的程序,通过阅读 Hazel.analyze 或 Hazel.run,将其错误报告为字符串,没有模块:
Hazel.analyze("let x : Int = true in x").errors;
// ["Error in term:\n true\nNature of error: Expecting type Int but got inconsistent type Bool"]
向他们展示
Hazel的消息是为Hazel的开发人员编写的。Dress Up表示一个人读到的类型错误,使用这个函数(its) scriptErrorText,略为缩短)。其他信息则按原样传递:
function errorText(message) {
const m = message.match(/^Error in term:\s*\n\s*([\s\S]*?)\nNature of error: Expecting type ([\s\S]*?) but got inconsistent type ([\s\S]*)$/);
if (!m) return message;
const a = t => (/^[AEIOU]/.test(t.trim()) ? "an " : "a ") + t.trim();
return `${m[1].trim()}: ${a(m[2])} is wanted here, but this is ${a(m[3])}`;
}
for (const e of result.errors) console.log(`${e.module ?? "the program"}: ${errorText(e.message)}`);
// TrapRoom: 3: a Side is wanted here, but this is an Int
当你展示给他们看时,要知道两件事:
- 输出一个没有名称的sum类型。 有
Side在模块中定义,而不是在序言中定义,消息会说L + R. 游戏将这些类型映射回自己的名称。 - 一个不存在的名字是一个缺失的标签。
Common.flop(L)给了Nature of error: Label not found,因为模块的成员可以像带标签的元组一样被访问。
游戏在三个地方显示一个房间的错误:在房间的盒子下面,按名字,在它的轮廓上有⚠️和一个计数,在它的事件日志中作为一个检查条目,在任何东西运行之前。
无论如何运行
runProgram 和 run 评估错误和漏洞,就像Hazel经常做的那样 value 有没有可能 ok 是假的。但是,如果错误或漏洞停止了计算,则返回值为 { hazel: "…" },它停在一个术语的打印输出,一个页面不能对它进行操作。mess Up Dress Up总是用它自己的JavaScript来回答问题,而房间的规则则不是 ok,并将黑兹尔的答案记录在旁边,这样游戏就可以在规则固定的情况下继续运行。欲了解更多,请参阅游戏 游戏脚本指南,第8节,关于破碎的脚本。
原型用例
乱穿打扮 是一款纯JavaScript的装扮冒险游戏,由两名年轻玩家制作。这是第一个黑兹莱特:它的五个房间把他们的事件交给一个规则,一个功能 on_event : (Globals, Event) -> Cmd,玩家可以在游戏的📜脚本面板中用Hazel(或JavaScript或Fumola)编写。游戏从面板的模块中创建一个程序 Hazel.runProgram,然后用每个事件调用房间规则,并运行它返回的命令。
黑兹尔的陷阱室规则,游戏旁边:
游戏和阅读同时进行:房间里的每个事件都交给黑兹尔,黑兹尔的回答也记录在小组的事件日志中:
游戏的脚本指南,更多的这些图片: 游戏脚本指南。
尺寸和速度
hazel-lib.js 约为2.8 MB,压缩后为640 KB。在笔记本电脑上的Node 22中测量: require("hazeljs") 大约需要170毫秒,运行一个小程序大约需要30毫秒。一个包含100个短构件的模块的分析时间约为110 ms,一个包含400个短构件的模块的分析时间约为2.3 s;编辑后,只读取文本更改的模块。
真的是黑兹尔吗?
hazel-lib.js 是2.8 MB的编译JavaScript,所以阅读它并不能告诉你什么。您可以检查的是,在您可以读取的提交中,它正是Hazel的源代码编译成的。
它命名它的提交。 Hazel.build 返回构建它的hazelgrove/hazel提交,以及 hazel 领域的 package.json 命名同一个提交,连同它的分支和拉请求。对于这个版本来说是这样的 0d66c36618ee709a60f91cf01fbf0b46121f2499。
重新构建并比较。 和一个克隆的 hazelgrove /榛 并设置其工具链(参见其README),运行
scripts/verify-build.sh path/to/hazel
它检出提交、构建 hazel-lib.js Hazel自己的部署工作流(发布概要文件)的方式 GITHUB_SHA 设置为提交),并比较两个文件。每个字节必须匹配,除了一个注释行, //# buildInfo:,其中js_of_ocaml为自己的版本标签命名(5.9.1 从一次安装开始, 5.9.1+5.9.1 从Hazel的锁文件)。没有这一行,这个版本的文件是SHA-256 4695853925917b8dc8514a78c3adbd3140a2277d79ab2c27612ce1595e08e856。
或者让GitHub重新构建它。 这个库的 验证hazel-lib.js workflow在GitHub的机器上运行相同的脚本,从Hazel的一个新克隆。任何人都可以从Actions选项卡中运行它,并且它的运行是公开的。
Hazel自己的CI也构建了它。 Hazel的Deploy工作流构建推送到hazelgrove/ Hazel的每个提交。它是这个提交(运行39680368678)给出了相同的散列,但没有buildInfo行。该构建是服务于 https://build.hazel.org/js-library-embed/hazel-lib.js 直到分支得到一个更新的提交:
curl -sL https://build.hazel.org/js-library-embed/hazel-lib.js | grep -v '^//# buildInfo:' | sha256sum
grep -v '^//# buildInfo:' node_modules/hazeljs/hazel-lib.js | sha256sum
这显示了文件的来源。它是Hazel的提交,由OCaml工具链在Hazel的锁文件中编译,仅此而已。它并不表明提交的代码是你想要的;为此,请阅读它,或者相信它的作者。
限制
- 实验。 还没有稳定的承诺;固定一个精确的版本。
- 同步的。 每个调用都在调用线程上运行,因此长时间运行的程序会阻塞它。
- 一个全球性的。 图书馆放了一个
Hazel全局对象上的。 - 用于慢读的控制台行。 只有Hazel的较慢的恢复解析器才能读取的文本(未闭括号,例如
let x = ()打印aSLOW PARSE (...)在读取时将行发送到控制台。 - 不能撤消或重做。 图书馆没有编辑器:它读取你给它的文本和答案。撤销和重做是Hazel编辑器的一部分,而不是这个库的一部分,因此保存文本的页面保留其历史:保留它拥有的文本(或每个模块的文本),并再次传递较旧的文本以返回。返回的代价是编辑所做的:重新读取自上次调用以来文本更改的模块,而其他模块则不读取。
- 错误命名一个术语,而不是一个地方。 一个错误的
id是Hazel语法树中对应项的id;图书馆还没有为它提供行和列。 - 卡住的值是调试输出。 如果出现错误或漏洞导致计算停止,
{ hazel }保存术语的OCaml打印输出,而不是Hazel语法。 - 深度程序需要更大的堆栈。 Hazel的分析深入递归:在Node中,一个大约有150个成员的模块溢出了默认堆栈(
Stack overflow). 提高:node --stack-size=32000 app.js,将壳自身的极限提高到与(ulimit -s 65536),否则Node会崩溃。
关于淡褐色
Hazel是由Hazel团队开发的:参见 hazel.org 对于这个项目,它的论文和制作它的人,还有 GitHub的贡献者 为代码工作的人。它的来源是 hazelgrove /榛,问题和bug就归它了 问题跟踪器。
许可证
MIT,就像Hazel一样。看到 许可证。