Trad 0.1.0 开发日志

发表于2019年03月26日

2019-05-12

从 lcui 模块中导入 App 对象到当前程序后,App 对象的路径就会变为当前程序的路径,这会导致编译器在解析 class MyApp extends App 时无法识别 MyApp 是继承自 lcui 模块的 App。

2019-05-05

用 import 语句导入模块里的对象时,如果完全解析一遍模块里的代码的话会增加编译耗时,考虑到现在解析代码的目的只是为了获取对象的声明,没必要像 JavaScript、Python 这类语言一样加载模块里全部代码,那么可以加个数据文件,功能类似于 C 中的头文件,只记录已导出的对象的声明。

追加需求如下:

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 类,则编译规则如下:

关于调整类方法参数列表的问题,有以下两种解决方案:

第二种方案将类方法的构建交给了解析器,比较灵活。

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");
}

// 省略其它代码 ...

从上述代码中可看出以下问题:

可以考虑添加一个 LCUI_Object 类型,用来记录数据类型、数据指针、析构函数、watcher 列表等,操作接口包括:创建、销毁、监听、解除监听、赋值等。

数据绑定的问题就不暂时不纠结了,先实现部件事件绑定功能。在 LCUI 中的部件事件处理函数有三个参数:绑定的部件、事件数据、触发器额外传入的数据,那么类的 _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

准备搞个语言来简化 LCUI 的界面开发,包括数据绑定、事件绑定、模板、css 样式等,这些功能用 C 来写的话很麻烦,用新语言来搞会简单很多。

编程风格有 Vue 和 React 两种可以参考:

综上所述,决定用 React 的风格,语法解析用 acorn 来做,第一个版本先用 JavaScript 的语法,以后再按需求进行扩展。

acorn 输出的数据是树形结构,而现在的做法并没考虑到深层结点嵌套的情况,需要做些调整,支持解析单个结点并递归解析子结点。

对此文章有疑问?你可以点击此链接反馈你的问题