Trad 0.1.0 开发日志
2019年03月26日 · 11384 字 · 阅读
之前看某个 Android 阵营的人在 LCUI 的评论区吹 Dart 语言和 Flutter 框架吹得挺带劲的,跟这类人争论只是浪费时间,即便争赢了他也不会帮你写代码。不过近期正好处于咸鱼模式,想搞点新东西,被他这么一说用 C 写 LCUI 应用程序的确很麻烦,要是有 JavaScript 这样的开发体验就好了,于是就决定搞个语言,搞出来后也能顺便推广 LCUI。为了纪念这位先进的程序员为推动此项目的开发而做出的杰出贡献,语言的名称就定为 Trad。
Trad 语言的定位是 C 语言的扩展,提供 class、async/await、闭包等新特性,以及更好的 UI 开发体验。
首先确定 UI 开发风格,能够参考的有 Vue 和 React 两种:
- Vue:Vue 组件的对象相当于一个配置集,配置包括 props、data、watch、methods 等,与其说是在写编译器,更像是在写 C 语言版本的 vue-loader,完全照搬 Vue 的编程方式感觉很没意思,而且开发复杂度看上去也高。
- React: 组件继承自 React.Component,可使用 JSX 语法描述界面,this.state 和 this.props 分别访问组件内部数据和外部传入的数据。class、类方法、state 和 props 的定义以及变量的赋值是主要解析目标,复杂度尚可,可以以此为机会来学习 React。
综上所述,决定采用 React 的风格,接下来是语法解析,从零开始写语法解析器太费时间,所以只能基于开源的语法树解析器来做,可选的有两种:
- TypeScript:粗略的看了下它的编译器 API 文档,感觉用起来有点复杂,要是基于 TypeScript 的语法的话,好像就没必要增加 C 语法兼容了,而且受众也变小了,正经的会 TypeScript 的前端开发工程师哪会浪费时间玩这个。
- Acorn:用起来比较容易,支持 JSX 语法。
决定用采用 Acorn,第一个版本先用 JavaScript 的语法,以后再按需求进行扩展。
2020-01-11
现在的语法树和生成器的实现代码是混在一起的,可以参考 babel 编译器的做法,将代码划分为语法树(AST)、解析器(Parser)、生成器(Generator)。
对于语法树的代码划分,可以参考 babel-types 和 babel-traverse,前者用于语法树结点的工具库,后者用于维护整棵树的状态,包括替换、移除和添加结点。
babel-generator 的 generators 目录存放着生成器的实现代码,这些生成器都被定义为与结点类型同名的函数,并由 printer.js 模块附加到 Printer.prototype 上,作为 Printer 类的方法。
2019-06-11
解析 class 时应该预先解析全部方法声明,然后再按优先级解析方法内的代码块。
2019-06-09
JSXExpressionContainer 的改进已经完成,接下来还剩两个任务:
-
允许在定义 state 和 props 时赋初始值,示例:
this.props = { value: 0, total: 100 } this.state = { input: 'hello', value: 10 }
-
给 TextEdit 的 value 属性添加读/写方法,示例:
// Input this.refs.textedit.value = 'hello' this.state.input = this.refs.textedit.value
// Output TextEdit_SetText(this.refs.textedit, "hello"); size_t len = TextEdit_GetTextLength(this.refs.textedit); char *text = malloc(sizeof(char) * (len + 1)); TextEdit_GetText(this.refs.textedit, text, len); String_SetValue(this.state.input, text); free(text);
获取 TextEdit 的内容比较麻烦,需要分配一块字符串缓存供
TextEdit_GetText()
写入内容,然后将缓存中的内容复制到this.state.input
中。可以考虑加个TextEdit_GetProperty()
函数来包装这些代码,然后输出的代码就能简化成这样:LCUI_Object value = TextEdit_GetProperty(this.refs.textedit, "value"); Object_Operate(this.state.input, "=", value); Object_Delete(value);
给部件属性赋值时会调用 operate() 方法输出对应的代码,还好这代码很简单,只有一个函数调用,没其它副作用,但取值就麻烦了,需要插入变量定义和函数调用表达式。现在有两种解决方案:
- 在插件层处理赋值表达式,如果右值是部件的属性,则转换为
TextEdit_GetProperty()
函数调用 - 为 CObject 添加 getter() 方法,在解析表达式时尝试调用变量的 getter() 方法
显然,第二个方案最为合适。
2019-06-08
部件内容的数据绑定已经完成,不过还得继续改进:
- 在解析 JSXExpressionContainer 时应该创建一个函数,然后将该函数作为解析器上下文,继续解析表达式
- 如果表达式是在部件属性中,则在函数末尾追加 Widget_SetAttrbute() 调用
- 如果表达式是在部件内容中,则在函数末尾追加 Widget_SetText() 调用
这么一说,JSXExpressionContainer 的解析结果是独立的,可以返回已创建的函数,在解析属性、内容和事件绑定时只需基于这个结果再创建一个包装函数就够了。
输入框是否有必要实现双向绑定?一般情况下按需主动取值也够用,第一个版本就不搞复杂的设计了。
在解析部件属性时,如果属性值类型是 JSXExpressionContainer,则在 state 属性中隐式创建名称以 _exp_
开头的对象并将部件的属性与之绑定。
JSXExpressionContainer 中有时只有一个标识符,例如:{ this.state.text }
,给它创建一个函数有点浪费,可以做个判断,如果子结点类型是 ThisExpression 且返回的对象是 CObject 类型则直接调用 Widget_BindProperty() 将它与属性绑定。
state 相关是在解析完 constructor() 后生成的,如果在解析 template() 时再追加属性的话还要手动添加初始化和数据绑定的代码,挺麻烦的,应该在解析完整个 class 后再输出 state 的相关代码。
2019-06-02
解析 const app = new MyApp()
时报错,定义了两个变量,变量赋值时调用 duplicate() 方法来输出对应的对象复制代码,但 MyApp 没有 duplicate() 方法。
这块功能是由 LCUI 插件实现的,需要做些调整:
- Number 和 String 类型改为从 LCUI 模块引入
- 整合 BinaryExpression、VariableDeclaration 等解析器到编译器中
- 对象的类型如果是继承自 CClass 则调用该类提供的方法输出 C 版本的代码,否则不做处理
- 对象在赋值时如果类方法没有实现 duplicate() 方法则默认输出为赋值表达式
CObject 有两种分配方式:从堆分配和从栈分配,两种方式对应 new() 和 init() 方法,new() 方法返回指针,init() 无返回值。假设 num 类型为 Number 且从栈分配,那么 const num = 0
的解析结果会是:
LCUI_ObjectRec num;
Number_Init(num, 0);
这些代码是在翻译阶段输出的,而代码格式化是在预翻译阶段进行的,如果变量声明多的话输出的代码看上去会很乱。
针对上述问题,补充几个规则:
- 仅在变量类型为基本类型时支持定义带初始值,例如:
int num = 123
- 类实例对象如果有初始值,则在解析阶段追加一个赋值表达式
2019-06-01
示例程序界面没样式,需要引入 css 文件,给 App 类加个 css 加载方法的话要起个名字,loadCSS?loadStyle?起名挺麻烦的,还要手动调用这个方法。
css 文件就由 import 语句来加载,规则如下:
- 如果当前文件中存在继承自 LCUI.Widget 的类,则给它的 install() 方法追加 LCUI_LoadCSSString() 函数调用
- 如果当前文件中存在继承自 LCUI.App 的类,则在构造函数中追加 LCUI_LoadCSSString() 函数调用
- 如果当前文件同时存在继承自 LCUI.App 和 LCUI.Widget 的类,则优先给 LCUI.App 处理
2019-05-27
类的方法和实例方法在编译时用的是同一命名方式,命名会冲突,在 C 代码中也看不出是类方法还是实例方法。可选的命名方式如下:
- 以 C 开头:
CProgress_Install()
- 类名以 Class 结尾:
ProgressClass_Install()
- 两个下划线:
Progress__Install()
自定义部件需要注册才能使用,那该如何注册?有以下两种方法:
- 隐式注册:在 App 的 template() 方法中解析 JSX 代码时,自动注册已使用的部件
- 显式注册:在 App 的构造函数中手动调用组件的 install() 方法
第一种只是写起来方便,但没法处理部件的先后依赖顺序,比如有三个部件依赖关系是 A->B->C,必须按顺序注册 C->B->A 才能正常使用部件,所以,选择第二种方法。
2019-05-25
解析对象的方法调用表达式时需要将 CMethod 保存到 CObject 对象里,应该由哪个属性来保存?目前也就 typeDeclaration 属性最适合保存 CMethod。
当 CCallExpression 类的 callee 成员类型为 CObject 时,函数的第一个参数应为 callee 的父级对象。
2019-05-20
在 C 中实现类继承有两种方法:
-
将基类对象作为子类的第一个成员。好处是可以共用同一块内存空间和地址,坏处是需要暴露基类的全部成员变量和构造/析构函数。
struct animal { int a; }; struct human { struct animal _animal; int a; }; void animal_init(struct animal *obj) { obj->a = 1; } void human_init(struct human *obj) { animal_init((struct animal*)obj); obj->a = 2; }
-
子类保存基类对象指针。好处是不用暴露基类的所有成员变量和构造/析构函数。
struct human { struct animal *_animal; int a; }; void animal_init(struct animal *obj); void human_init(struct human *obj) { animal_init(obj->_animal); obj->a = 2; }
2019-05-19
添加 CBinaryExpression 时遇到问题,将调用表达式(CCallExpression)跟对象(CObject)比较时该如何处理?调用表达式的 define() 方法是输出函数调用代码,而 CObject 的 define() 方法则是输出对象的定义,编译时都调用 define() 方法的话结果会是这样:add(a, b) > int b
,两边都加上判断的话写起来又挺麻烦的,看上去需要再添加一个方法。
2019-05-18
App 和 Widget 都需要有数据绑定、事件绑定、JSX 语法支持,两个功能不同的类有相同的功能,实现起来有点让人纠结,目前有两种实现方法可选:
- 拆成两个解析器处理,共用部分相同功能的代码
- 解析 App 类时隐式创建一个 Widget 类,例如:AppRootWidget,将数据绑定、事件绑定、JSX 等相关代码作为 Widget 类的代码来解析
Widget 的 update() 方法是在更新部件时调用的,那 App 的 update() 方法该在什么时候调用?
2019-05-16
将 LCUI_Object
类翻译为 C 代码后,类方法都会有 LCUI_Object_
前缀,LCUI 的命名风格是只有类型带 LCUI_
前缀,而相关函数由于我个人嫌写起来麻烦所以大都没加 LCUI_
前缀。看样子需要提前引入 namespace 了,引入后改动如下:
- 给 CIdentifier 类添加 namespace 属性
- 给 CIdentifier 类添加 cName 属性,表示对应 C 代码中的名称
- 给 CIdentifier 类添加 useNamespace 属性,用于控制是否将 namespace 作为名称前缀
- 给 CClass 类添加 useNamespaceForMethods 属性,用于控制是否将 namespace 作为类方法名称的前缀
2019-05-13
CIdentify 类的大部分数据都是存储在 body 上,给它加个 reference 属性,在该属性有效时则将自身的 body 属性映射为 reference 的 body 属性,这样就能实现类似于指针的功能了。
在解析 import 语句时为导入的对象创建一个引用,编译器可以通过 App.reference.modulePath === 'lcui'
来判断标识符 App 是否引用自 lcui 模块里的对象。
2019-05-12
有如下代码,在解析到类声明时如何判断 App 是来自 lcui 模块?
import App from 'lcui'
class MyApp extends App {}
现在每个对象都有一个路径,在解析 lcui 模块时 App 的路径是 lcui/App,但导入到当前程序后会变为 App,编译器无法识别 App 是来自 lcui 模块。
2019-05-05
用 import 语句导入模块里的对象时,如果完全解析一遍模块里的代码的话会增加编译耗时,考虑到现在解析代码的目的只是为了获取对象的声明,没必要像 JavaScript、Python 这类语言一样加载模块里全部代码,那么可以加个数据文件,功能类似于 C 中的头文件,只记录已导出的对象的声明。
追加需求如下:
- 编译时追加输出 .json 文件,用于记录模块内已导出的对象的定义,节省编译时间
- import 解析器在导入模块时优先读取 .json 文件,只导入必要的对象及依赖模块,例如:foo 模块中有 a, b,c 对象,其中 c 对象依赖 bar 模块,现在只导入 foo 模块中的 a 和 b 对象,那么不会导入依赖的 bar 模块
2019-05-04
Widget 有 template() 和 update() 这两个方法,template() 用于声明界面结构、数据绑定和事件绑定,而 update() 用于集中处理与数据相关的一些部件操作。每当数据变化时会标记需要更新,等下一帧部件更新时会调用 update() 方法来更新。
Widget 类的 template() 方法中的根级部件是自身,而 App 类则是新的部件。
2019-05-03
Number 和 String 都继承自 LCUI_Object
,如果将 String_Init()
、String_New()
等专属方法定义为 CMethod 的话,基类的 Object_Operate()
、Object_ToString()
等方法名的前缀都会变为 String_
。只能将这类专属方法定义为 CFunction,保留现有的命名。
类的名称是 LCUI_Object
,输出的类方法名会带 LCUI_Object_
前缀,需要加个成员变量来自定义类方法名前缀。
用 CTypedef 为 CClass 定义一个别名后,所有方法都应该映射到目标 CClass 的方法上,手动写代码一个个映射很麻烦,判断类型时还要专门判断是不是 CTypedef,是的话则获取真实的类型定义。
2019-04-27
如果类继承自 Widget 类,则编译规则如下:
- 类方法的第一个参数都是 LCUI_Widget 类型的部件指针
- 类方法中的 this 表达式解析为部件私有数据的指针 _this
- 类方法中的第一行代码为 Widget_GetData() 函数调用,以获取私有数据的指针 _this
- constructor() 和 destructor() 方法会绑定到部件原型上
- 默认创建部件注册方法,命名格式为:
LCUIWidget_Add{部件类名}
关于调整类方法参数列表的问题,有以下两种解决方案:
- 让 CClass 的 createMethod() 方法支持自定义方法的构造器:
- 添加一个继承自 CMethod 的 CLCUIWidgetMethod 类,覆写相关方法,在它输出参数列表时插入第一个参数
- 在 CClass 里添加 methodClass 成员变量,将 createMethod() 方法改为使用 this.methodClass 作为对象构造器来创建方法
- 由解析器来创建类方法:
- 调整 Class 的解析规则,不预先为类创建全部方法,改为在解析 MethodDefinition 结点时创建方法
- 移除 CClass 类的 createMethod() 方法,添加 addMethod() 方法
- 添加一个继承自 CMethod 的 CLCUIWidgetMethod 类,覆写相关方法,在它输出参数列表时插入 LCUI_Widget 类型的对象作为第一个参数
- LCUI 插件中添加 MethodDefinition 解析器,返回 CLCUIWidgetMethod 对象
第二种方案将类方法的构建交给了解析器,比较灵活。
Widget 构造方法和普通的类不一样,那么 CClass 的 createNewMethod() 和 createDeleteMethod() 方法应该改为由 Class 解析器来实现。
JavaScript 的 class 的 setter 和 getter 要成对存在,不能只继承基类中的 getter 或 setter。
2019-04-21
基于树的结构实现追加、查询、遍历操作就够了,现在整的 types、typesDict、scope、owner、module、parent 等成员变量只是在增加复杂度。
2019-04-11
props 和 state 解析器会互相调用导致栈溢出,两个解析器都有 parseMethodDefinition() 方法,props.js 里调用 super.parse() 时会调用 state.js 的 parse() 方法,而 state.js 调用 super.parse() 又会回到 props.js,一直循环。
RangeError: Maximum call stack size exceeded
at LCUIParser.parse (c:\Users\LC\Documents\GitHub\trad\src\plugins\lcui\state.js:1:1)
at LCUIParser.parseMethodDefinition (c:\Users\LC\Documents\GitHub\trad\src\plugins\lcui\props.js:85:26)
at LCUIParser.parse (c:\Users\LC\Documents\GitHub\trad\src\plugins\lcui\state.js:106:28)
at LCUIParser.parseMethodDefinition (c:\Users\LC\Documents\GitHub\trad\src\plugins\lcui\props.js:85:26)
at LCUIParser.parse (c:\Users\LC\Documents\GitHub\trad\src\plugins\lcui\state.js:106:28)
at LCUIParser.parseMethodDefinition (c:\Users\LC\Documents\GitHub\trad\src\plugins\lcui\props.js:85:26)
原因是 this 指向的是继承类,当前类没有指定方法时会往基类找,在 state.js 找到方法后,调用 super.parse() 实际是调用 props.js 的 parse()。解决方法是显式调用类原型里的方法,例如:
class StateBindingParser extends Compiler {
// ...
parse(input) {
const method = 'parse' + input.type
if (StateBindingParser.prototype.hasOwnProperty(method)) {
return StateBindingParser.prototype[method].call(this, input)
}
return super.parse(input)
}
// ...
}
2019-04-06
class 结构体中的结构体成员应该用实体,而不是指针,这样只需为 class 结构体分配一次内存,省得再调用 malloc() 为结构体成员分配内存。
做些调整,CStruct 和 CClass 用于定义数据结构,CObject 用于定义对象,创建 CObject 对象时会记录所属的类型,然后根据该类型来构造属性列表。
解析 class 时应该先解析所有方法声明,然后再逐个解析方法内的代码块,不然在给部件添加事件绑定时会找不到事件处理函数。
LCUI 组件的数据绑定已经完成,接下来是事件绑定。
2019-04-05
解析 this 的成员表达式时,如果成员在这之前没有定义,则会为该成员定义一个结构体,但是现在的结构体是用 CStruct 对象表达的,既可以是类型也可以是对象,解析时也不好判断是应该定义结构体,还是应该定义结构体的对象,嵌套的结构体处理起来更麻烦。
2019-04-04
开始添加 LCUI_Object 数据结构。
2019-04-03
有些纠结要不要在这个版本中加入数据绑定功能,第一个版本搞这么复杂的功能会增加很多时间成本,以 LCFinder 为例,如果要加的话,需要解决以下问题:
- 如何高效渲染包含上万张缩略图的列表?
- 增加和删除若干个图片后,如何高效检测出列表中哪些部件需要改动?
- 数据类型不同,操作方法都不同,如何设计统一的接口和数据结构?
示例代码如下,text 对象被绑定到了两个部件上,hello() 方法中有 text 的赋值操作。
class MyWidget extends LCUI.Widget {
constructor() {
this.state = {
text: String
}
}
template() {
return <Widget>
<TextView>{this.state.text}</TextView>
<TextEdit value={this.state.text} />
</Widget>
}
hello() {
this.state.text = 'hello'
}
}
刚开始的想到的实现方法是将对象的写操作替换为 set() 函数调用,然后将绑定的部件操作放到 set() 函数里,为方便操作部件,会默认记录部件的指针,那么上面的代码翻译成 C 代码大致是这样的:
typedef struct MyWidgetRec_* MyWidget;
typedef struct MyWidgetRec_ MyWidgetRec;
struct MyWidgetRec_ {
struct {
LCUI_Widget _textview;
LCUI_Widget _textedit;
} refs;
struct {
char *text;
} state;
};
// 省略其它代码 ...
// state 的操作函数都有 `$` 前缀且始终用 static 修饰,与其它函数隔离
static void MyWidget_$SetText(MyWidget _this, const char *text)
{
_this.state.text = text;
// 部件原型中有 settext 函数指针,每个部件都可以设置它,所以直接调用 Widget_SetText() 函数
Widget_SetText(_this->refs._textview, text);
// 绑定了 value 属性,说明 TextEdit 有处理方法,所以调用 TextEdit_SetValue()
TextEdit_SetValue(_this->refs._textedit, text);
}
static void MyWidget_Hello(MyWidget _this)
{
MyWidget_$SetText(_this, "hello");
}
// 省略其它代码 ...
从上述代码中可看出以下问题:
- 数据绑定功能太依赖编译器,以后要实现手动监听数据变化或类似于 Vue 的计算属性功能会很麻烦
- 存在内存泄漏问题,无法判断是否需要释放字符串
可以考虑添加一个 LCUI_Object 类型,用来记录数据类型、数据指针、析构函数、watcher 列表等,操作接口包括:创建、销毁、监听、解除监听、赋值等。
数据绑定的问题就不暂时不纠结了,先实现部件事件绑定功能。在 LCUI 中的部件事件处理函数有三个参数:绑定的部件、事件数据、触发器额外传入的数据,那么类的 _this 指针怎么传入?有两种做法:
- 放到 event->data 中。event->data 比较常用,在 LCFinder 中,每个图片部件都绑定了 click 事件,event->data 存的是与图片对应的文件信息,要是占用的话以后可能又需要改。
- 封装一次部件处理函数,将 _this 作为第三个参数传入。包装挺麻烦的,要创建一个结构体存储当前 this 指针和事件处理函数,再创建一个函数来转发事件给目标处理函数。
2019-03-31
在 React 中修改数据时需要调用 this.setState()
方法,然后在回调函数中修改状态并将新状态返回出去,这种机制要翻译成 C 代码的话复杂度有点高,还是改用 Vue 的做法吧,允许直接修改属性,等下一帧再批量更新与数据绑定的组件,实现起来简单些,只需要在翻译阶段推断出对象的类型,然后再将赋值语句翻译为对应的函数调用。
需要与组件绑定的数据都放到 state 里,props 中存放外部传入的数据,与其它的类成员隔离。
在翻译 import 语句时只是简单的替换成 #include
,没对目标文件进行解析,导入的对象都是默认为 CObject 对象。以后可能需要加上解析功能,输出模块内导出的对象信息到文件里,类似于头文件,以节省以后的解析时间。
2019-03-29
在解析结点后直接输出结果的话不方便后续调整,例如:当 class 有继承基类但没有构造函数时,应该添加一个默认的构造函数;解析完后,需要将 export 修饰的对象和函数输出到头文件里。也就是说,应该在编译前加个解析步骤,将 acorn 解析好的语法树再解析为适合输出 C 代码的语法树,然后再经过编译输出为 C 代码。
解析 ReturnStatement 时需要知道 return 后面的内容是什么,如果是函数调用,那么在解析函数调用时还得判断上级解析器是不是 return 语句,是则不写入结果,而是以返回值形式将结果返回给 ReturnStatement 解析器,让它组装成完整的 return 语句。
2019-03-28
acornjs 的插件是在调用时继承 Parser 类的,要包装一层函数,多了一层缩进,看着难受。手动改 class 的 prototype 来改变父类虽然可行,但不能用 super 来调用基类的方法,会报语法错: SyntaxError: 'super' keyword unexpected here
。
module.exports = function noisyReadToken(Parser) {
return class extends Parser {
readToken(code) {
console.log("Reading a token!")
super.readToken(code)
}
}
}
2019-03-27
与 LCUI 的相关的解析行为可以独立成插件,这样能方便其他人参考添加其它插件。
2019-03-26
acorn 输出的数据是树形结构,而现在的做法并没考虑到深层结点嵌套的情况,需要做些调整,支持解析单个结点并递归解析子结点。
文章版权归作者所有,未经许可不得转载。