LiteLDev / LegacyScriptEngine

A plugin engine for running LLSE plugins on LeviLamina
GNU General Public License v3.0
48 stars 8 forks source link

根据统一接口定义自动生成补全库(以及文档?) #164

Closed yqs112358 closed 2 months ago

yqs112358 commented 1 year ago

LLSE在引擎层采用跨语言统一接口设计,但是不同语言后端的补全库需要单独手动维护(js/lua/py),工作量较大且容易出现遗漏。另外文档编写和修改也较为麻烦。

设想: 使用某种统一接口定义语言(如TypeScript接口)对所有API进行建模,然后写程序解析AST并翻译到各目标语言的补全文件。以后变动API时,除了修改代码之外,只需要对接口文件进行修改,并执行程序更新补全库即可(可由github actions自动化)

另外如果可能的话,在接口文件中嵌入文本段,并用于自动化生成文档,以降低文档维护工作量

cvxz8 commented 10 months ago

感觉涉及到复杂类型时,难以进行翻译 例如Typescript中的交叉、联合类型、keyof、typeof、条件类型、类型推断,难以在python中表示

当然,仅提供API的类型时,可能涉及不到复杂的类型定义,可以考虑忽略复杂类型翻译

harry-xi commented 9 months ago

假定这个API建模手动完成,或许考虑选取一个与特定语言无关的形式来提供这个内容。 这样子,对与一些复杂类型,我们可以采取类似下面这个形式。同时,这么做或许可以更易于解析和处理

[class]
docs = '''
文档
'''
# 选择toml举例是因为它的多行字符串功能会文档内嵌提供不小的便利
[class.methodsA]
 # 一个带有复杂类型声明的方法
docs = '''
文档内容
'''
type.ts = ""
type.py = ""
type.lua = ""
# 把这些字符分别替换给对应语言
[class.methodsB]
# 一个简单的类型声明
docs='''
文档内容
'''
type=""
# 这里或许应该采取别的形式,而非字符串,但是我暂时没有进一步的思考

一个额外的思考,有没有那个语言从声明文件生成的文档可以直接满足ls项目文档的需求,他们通常会提供一些极有价值的功能 (虽然似乎如果需要,我们可以在自己实现文档生成的时候自己实现那些功能

另外一个问题,我们有多大可能自动化的从源码生成API类型声明。

cvxz8 commented 9 months ago

像OneLang、Haxe、Progsbase这些工具似乎都不能同时支持所需的三种语言 简单的试用过后感觉对接口定义的翻译不是很友好(该点存疑,若有错误请懂行的大佬指正)

或许可以用json或yml等形式,直接编写一个自定义的语法树 该树类似AST但只包含主要需要的定义,这些定义大多有高度概括性,或者只是一种大概的模糊定义 具体的代码生成则由对应目标语言的翻译器实现

对于本项目来说,一些语言特性与复杂类型可能用不到,因此翻译器只需要实现主要语法翻译即可 同时因为这个语法树的概括性,对于文档生成有一定帮助

cvxz8 commented 9 months ago

像OneLang、Haxe、Progsbase这些工具似乎都不能同时支持所需的三种语言 简单的试用过后感觉对接口定义的翻译不是很友好(该点存疑,若有错误请懂行的大佬指正)

或许可以用json或yml等形式,直接编写一个自定义的语法树 该树类似AST但只包含主要需要的定义,这些定义大多有高度概括性,或者只是一种大概的模糊定义 具体的代码生成则由对应目标语言的翻译器实现

对于本项目来说,一些语言特性与复杂类型可能用不到,因此翻译器只需要实现主要语法翻译即可 同时因为这个语法树的概括性,对于文档生成有一定帮助

这里的概括性是对于不同目标语言来说的,例如语言1和语言2中的类型分别是T1和T2,语法树中则可以使用T表示。目标语言翻译器在翻译时自动转换

superx101 commented 8 months ago

写了一个小demo,尝试在typescript的定义文件中加入一些规则,来统一生成多种语言的类型定义文件。

demo地址:https://github.com/superx101/TypeLoom

这个demo把typescript的AST转换为了一个自定义的简单AST,并解析这个AST实现目标代码生成 目前仅实现了d.ts解析,和d.ts生成。 在解析时可以顺便生成一份键值对文件,用于国际化。解析AST时则可以读取这个键值对文件替换成其他自然语言。

用一个样例可以快速解释该demo的内容: 首先手动建模一份定义文件,如下:

// 使用特定的类型名称来定义基本类型
// 目前解析器中会忽略所有的类型别名定义,这里仅仅为了编译器不报错,类型取值随意
declare type dint = number;
declare type dfloat = number;
declare type dvoid = void;
declare type dstring = string;
declare type dany = any;
declare type dbool = boolean;

/**
 * 在生成文档、注释时忽略这个命名空间
 * @diable
 */
declare namespace exampleNamespace1 {
    /**
     * 在生成文档、注释时忽略这个接口
     * @disable
     */
    interface Interface1 {
        /**
         * 为属性添加一段描述。
         * 这个标签默认开启
         * @text
         */
        i1p1: dint;
    }

    /**
     * 标记为弃用
     * @deprecated
     */
    interface Interface2 {
        /**
         * @deprecated
         * 
         * 为属性添加一个示例
         * @example
         */
        i2p1: dint;
        /**
         * 在生成注释时,为每个函数属性都添加注释
         * @params
         * 
         * 同理,添加返回值注释
         * @returns 
         */
        i2p2(p1: dint, p2: dstring): dint;
    }

    class Class1 {
        // 如果函数没有注释,则按照默认标签处理,这里也会加上 params 和 returns
        constructor(p1: dint, p2: dbool);
        /**
         * 在标签后面添加false,显式的禁用标签。若不指明true/false则默认true
         * @returns false
         */ 
        c1m1(): dint;
        /**
         * 同理,显式开关标签
         * @params false
         * @returns true
         */
        c1m2(p1: dint, p2: dstring): dint;
        /**
         * text是个可重复标签,可多次使用
         * @text
         * @params
         * @returns
         * @text
         */
        c2m3(p1: dint, p2: dstring): dint;
    }
}

使用demo中的DTSParser,把文件解析为AST。AST的结构可见: https://github.com/superx101/TypeLoom/blob/main/example/1-getting-started/output/ast-json.json 为了方便理解,把一些AST中的枚举替换为了字符串

同时让demo对AST遍历生成一个语言文件,这部分功能demo中已封装好。语言文件如下:

{
    "source-d-ts.example-namespace1.interface1.i1p1.text0": "",
    "source-d-ts.example-namespace1.interface1.i1p1.text1": "",
    "source-d-ts.example-namespace1.interface2.i2p1.text0": "",
    "source-d-ts.example-namespace1.interface2.i2p1.deprecated": "",
    "source-d-ts.example-namespace1.interface2.i2p1.example.ts": "",
    "source-d-ts.example-namespace1.interface2.i2p2.text0": "",
    "source-d-ts.example-namespace1.interface2.i2p2.param.p1": "",
    "source-d-ts.example-namespace1.interface2.i2p2.param.p2": "",
    "source-d-ts.example-namespace1.interface2.i2p2.returns": "",
    "source-d-ts.example-namespace1.interface2.text0": "",
    "source-d-ts.example-namespace1.interface2.deprecated": "",
    "source-d-ts.example-namespace1.class1.constructor.text0": "",
    "source-d-ts.example-namespace1.class1.constructor.param.p1": "",
    "source-d-ts.example-namespace1.class1.constructor.param.p2": "",
    "source-d-ts.example-namespace1.class1.c1m1.text0": "",
    "source-d-ts.example-namespace1.class1.c1m1.returns": "",
    "source-d-ts.example-namespace1.class1.c1m2.text0": "",
    "source-d-ts.example-namespace1.class1.c1m2.param.p1": "",
    "source-d-ts.example-namespace1.class1.c1m2.param.p2": "",
    "source-d-ts.example-namespace1.class1.c1m2.returns": "",
    "source-d-ts.example-namespace1.class1.c2m3.text0": "",
    "source-d-ts.example-namespace1.class1.c2m3.param.p1": "",
    "source-d-ts.example-namespace1.class1.c2m3.param.p2": "",
    "source-d-ts.example-namespace1.class1.c2m3.returns": "",
    "source-d-ts.example-namespace1.class1.c2m3.text1": "",
    "source-d-ts.example-namespace1.class1.c2m3.text2": "",
    "source-d-ts.example-namespace1.class1.text0": "",
    "source-d-ts.example-namespace1.text0": "",
    "source-d-ts.example-namespace1.diable": ""
}

最后让目标代码生成器,装入AST和这个语言文件,得到目标代码的d.ts文件。这里因为没有为语言文件填入内容,得到的代码中打印的是语言文件的键。在实际使用中也可以先输出一个带key的代码,在语言文件中对照翻译后再生成翻译后的代码。

当前demo中的目标代码生成器,会把所有涉及到的d.ts文件自动打包为一个d.ts

代码如下:


// file: source-d-ts

//这里是一个错误,标签disable拼写错误。demo中没有定义的标签依然可以被当做一个普通标签输出
/**
 * source-d-ts.example-namespace1.text0
 * @diable source-d-ts.example-namespace1.diable
 */
declare namespace exampleNamespace1 {
    interface Interface1 {
        /**
         * source-d-ts.example-namespace1.interface1.i1p1.text0
         * source-d-ts.example-namespace1.interface1.i1p1.text1
         */
        i1p1: number;
    }
    /**
     * source-d-ts.example-namespace1.interface2.text0
     * @deprecated source-d-ts.example-namespace1.interface2.deprecated
     */
    interface Interface2 {
        /**
         * source-d-ts.example-namespace1.interface2.i2p1.text0
         * @deprecated source-d-ts.example-namespace1.interface2.i2p1.deprecated
         * @example source-d-ts.example-namespace1.interface2.i2p1.example.ts
         */
        i2p1: number;
        /**
         * source-d-ts.example-namespace1.interface2.i2p2.text0
         * @param p1 source-d-ts.example-namespace1.interface2.i2p2.param.p1
         * @param p2 source-d-ts.example-namespace1.interface2.i2p2.param.p2
         * @returns source-d-ts.example-namespace1.interface2.i2p2.returns
         */
        i2p2(p1: number, p2: string): number;
    }
    /**
     * source-d-ts.example-namespace1.class1.text0
     */
    class Class1 {
        /**
         * source-d-ts.example-namespace1.class1.constructor.text0
         * @param p1 source-d-ts.example-namespace1.class1.constructor.param.p1
         * @param p2 source-d-ts.example-namespace1.class1.constructor.param.p2
         */
        constructor(p1: number, p2: boolean);
        /**
         * source-d-ts.example-namespace1.class1.c1m1.text0
         * @returns source-d-ts.example-namespace1.class1.c1m1.returns
         */
        c1m1(): number;
        /**
         * source-d-ts.example-namespace1.class1.c1m2.text0
         * @param p1 source-d-ts.example-namespace1.class1.c1m2.param.p1
         * @param p2 source-d-ts.example-namespace1.class1.c1m2.param.p2
         * @returns source-d-ts.example-namespace1.class1.c1m2.returns
         */
        c1m2(p1: number, p2: string): number;
        /**
         * source-d-ts.example-namespace1.class1.c2m3.text0
         * @param p1 source-d-ts.example-namespace1.class1.c2m3.param.p1
         * @param p2 source-d-ts.example-namespace1.class1.c2m3.param.p2
         * @returns source-d-ts.example-namespace1.class1.c2m3.returns
         * source-d-ts.example-namespace1.class1.c2m3.text1
         * source-d-ts.example-namespace1.class1.c2m3.text2
         */
        c2m3(p1: number, p2: string): number;
    }
}

更加详细的示例请见:https://github.com/superx101/TypeLoom/tree/main/example/1-getting-started

该demo中还支持了简单的泛型定义、简单的泛型约束、继承、实现接口,而泛型推断不打算支持,因为部分语言没有ts这样的类型能力,且对于API定义来说,用不到泛型推断

superx101 commented 8 months ago

另外一个问题,我们有多大可能自动化的从源码生成API类型声明。

这是有很大可能的,虽然C++语法解析很复杂,但是可以通过正则匹配特定语句段,在语句段中使用自定义的文法分析器来提取源码中的声明。然后把提取到的声明构建为TypeLoom中的AST,就能通过TypeLoom生成d.ts的定义文件

另外如果可能的话,在接口文件中嵌入文本段,并用于自动化生成文档,以降低文档维护工作量

文档生成也可看做目标代码生成,对AST编写文档生成器,也可能直接从源码生成补全库、文档