里程碑!用自己的编程语言实现了一个网站!

44e3bd9ef071244fdcb6d20966da66bd.jpeg

前言

自制编程语言,网上的文章多如牛毛,但是真正能坚持着把一个语言给实现了,同时还能写一个应用程序员的就很少了。

大部分网上博文一般只写到词法分析、语法分析都比较少,后面就没有下文了。

这件事还是有不小门槛的,实现了编程语言,还得去实现相关类库,要不然它就是一个没多大用处的玩具。没有兴趣驱使着自己往前走,往往是坚持不下来的。

今天介绍一下@crossoverJie开发的编程语言:GScript。

他已经用自制的编程语言已经达到了一个小里程碑:开发了一个网站。

https://gscript.crossoverjie.top/index

363aa1a07aa7f263ba3e9c0eb800fd4d.jpeg

要称为一个网站确实有点勉强,不过也是一个动态网页,因为返回的是 HTML,所以在当前阶段只要不嫌麻烦其实也能写一个“合格”的网站,有点像以前我们学习 Java 时的 servlet

该页面的源码地址在这里:https://github.com/crossoverjie/gscript-homepage

其实总共也就40来行代码:

class GScript{
    string author;
    string[] features;
    string since;

    GScript(string a, string[] f, string s){
        author = a;
        features = f;
        since = s;
    }
}

func (HttpContext) index(HttpContext ctx){
    string[] features = {"statically", "strongly"};
    GScript gs = GScript("crossoverJie",features, "2022");
    string j = JSON(gs);
    println(j);
    string local = getCurrentTime("Asia/Shanghai","2006-01-02 15:04:05");
    println("local=" + local);
    string html = ^
        <html>
            <title>GScript</title>
            <pre>
                 _     _   
 ___ ___ ___ ___|_|___| |_ 
| . |_ -|  _|  _| | . |  _|
|_  |___|___|_| |_|  _|_|  
|___|             |_|   v0.0.7   

^+ j +^
            </pre>
            <h1>current ^+ local +^</h1>
            <p><a href="https://github.com/crossoverjie/gscript-homepage">GScript-homepace source code</a></p>
        </html>
    ^;
    ctx.HTML(200, html);
}

httpHandle("GET", "/index", index);
string[] args = getOSArgs();
if (len(args) ==3){
    httpRun(":" + args[2]);
}else {
    httpRun(":8000");
}

全是利用 GScript 所提供的标准库实现的,后文会详细聊聊内置 HTTP 包。

特性

首先来看看保留环节, GScript 是如何编写 hello world 的。

hello_world.gs:

println("hello world");
❯ gscript hello_world.gs
hello world

废话说完了接下来重点聊聊 GScript 所支持的特性了。

1109eff2f2c7e84ab0449e311497dabb.jpeg后文会重点说明每一个特性。

例子

除了刚才提到的 hello world,再来看一个也是示例代码经常演示的打印斐波那契数列

void fib(){
    int a = 0;
    int b = 1;
    int fibonacci(){
        int c = a;
        a = b;
        b = a+c;
        return c;
    }
    return fibonacci;
}
func int() f = fib();
for (int i = 0; i < 5; i++){
    println(f());
}

输出结果如下:

0
1
1
2
3

整体写法与 Go 官方推荐的类似:https://go.dev/play/p/NeGuDahW2yP

// fib returns a function that returns
// successive Fibonacci numbers.
func fib() func() int {
 a, b := 0, 1
 return func() int {
  a, b = b, a+b
  return a
 }
}
func main() {
 f := fib()
 // Function calls are evaluated left-to-right.
 fmt.Println(f(), f(), f(), f(), f())
}

都是通过闭包变量实现的,同时也展示了 GScript 对闭包、函数的使用,后文详细介绍闭包的用法。

语法

GScript 的语法与常见的 Java/Go 类似,所以上手非常简单。

基本类型

先来看看基本类型,目前支持 int/string/float/bool 四种基本类型以及 nil 特殊类型。

变量声明语法和 Java 类似:

int a=10;
string b,c;
float e = 10.1;
bool f = false;

个人觉得将类型放在前面,代码阅读起来会更清晰一些,当然这也是个人喜好。

数组

// 声明并初始化
int[] a={1,2,3};
println(a);

// 声明一个空数组并指定大小
int[] table = [4]{};

println();
// 向数组 append 数据
a = append(a,4);
println(a);
for(int i=0;i<len(a);i++){
 println(a[i]);
}

// 通过下标获取数组数据
int b=a[2];
println(b);

其实严格来讲这并不算是数组,因为它的底层是用 Go 切片实现的,所以可以动态扩容。

以这段代码为例:

int[] a=[2]{};
println("数组大小:"+len(a));
a = append(a,1);
println("数组大小:"+len(a));
println(a);
a[0]=100;
println(a);

输出:

数组大小:2
数组大小:3
[<nil> <nil> 1]
[100 <nil> 1]

Class

类的支持非常重要,是实现面向对象的基础,目前还未完全实现面向对象,只实现了数据与函数的封装。

class ListNode{
    int value;
    ListNode next;
    ListNode(int v, ListNode n){
        value =v;
        next = n;
    }
}

// 调用构造函数时不需要使用 new 关键字。
ListNode l1 = ListNode(1, nil);

// 使用 . 调用对象属性或函数。
println(l1.value);

缺省情况下 class 具有无参构造函数:

class Person{
 int age=10;
 string name="abc";
 int getAge(){
  return 100+age;
 }
}

// 无参构造函数
Person xx= Person();
println(xx.age);
assertEqual(xx.age, 10);
println(xx.getAge());
assertEqual(xx.getAge(), 110);

得益于 class 的实现,结合刚才的数组也可以定义出自定义类型的数组:

// 大小为 16 的 Person 数组
Person[] personList = [16]{};

函数

函数其实分为两类:

  • 普通的全局函数。

  • 类的函数。

本质上没有任何区别,只是所属范围不同而已。

// 判断链表是否有环
bool hasCycle(ListNode head){
    if (head == nil){
        return false;
    }
    if (head.next == nil){
        return false;
    }

    ListNode fast = head.next;
    ListNode slow = head;
    bool ret = false;
    for (fast.next != nil){
        if (fast.next == nil){
            return false;
        }
        if (fast.next.next == nil){
            return false;
        }
        if (slow.next == nil){
            return false;
        }
        if (fast == slow){
            ret = true;
            return true;
        }

        fast = fast.next.next;
        slow = slow.next;
    }
    return ret;
}

ListNode l1 = ListNode(1, nil);
bool b1 =hasCycle(l1);
println(b1);
assertEqual(b1, false);

ListNode l4 = ListNode(4, nil);
ListNode l3 = ListNode(3, l4);
ListNode l2 = ListNode(2, l3);
bool b2 = hasCycle(l2);
println(b2);
assertEqual(b2, false);

l4.next = l2;
bool b3 = hasCycle(l2);
println(b3);
assertEqual(b3, true);

这里演示了链表是否有环的一个函数,只要有其他语言的使用基础,相信阅读起来没有任何问题。

add(int a){}

当函数没有返回值时,可以声明为 void 或直接忽略返回类型。

闭包

闭包我认为是非常有意思的一个特性,可以实现很灵活的设计,也是函数式编程的基础。

所以在 GScript 中函数是作为一等公民存在;因此 GScript 也支持函数类型的变量。

函数变量声明语法如下:func typeTypeOrVoid '(' typeList? ')'

// 外部变量,全局共享。
int varExternal =10;
func int(int) f1(){
 // 闭包变量对每个闭包单独可见
 int varInner = 20;
 int innerFun(int a){
  println(a);
  int c=100;
  varExternal++;
  varInner++;
  return varInner;
 }
 // 返回函数
 return innerFun;
}

// f2 作为一个函数类型,接收的是一个返回值和参数都是 int 的函数。
func int(int) f2 = f1();
for(int i=0;i<2;i++){
 println("varInner=" + f2(i) + ", varExternal=" + varExternal);
}
println("=======");
func int(int) f3 = f1();
for(int i=0;i<2;i++){
 println("varInner=" + f3(i) + ", varExternal=" + varExternal);
}

最终输出如下:

0
varInner=21, varExternal=11
1
varInner=22, varExternal=12
=======
0
varInner=21, varExternal=13
1
varInner=22, varExternal=14
func int(int) f2 = f1();

以这段代码为例:f2 是一个返回值,入参都为 int 的函数类型;所以后续可以直接当做函数调用 f2(i).

例子中将闭包分别赋值给 f2 和 f3 变量,这两个变量中的闭包数据也是互相隔离、互不影响的,所有基于这个特性甚至还是实现面向对象。

关于闭包的实现,后续会单独更新一篇。

更多样例请参考:https://github.com/crossoverJie/gscript/tree/main/example

标准库

标准库源码:https://github.com/crossoverJie/gscript/tree/main/internal

目前实现的标准库并不多,这完全是一个体力活;基于现有的语法和基础数据类型,几乎可以实现大部分的数据结构了,所以感兴趣的朋友也欢迎来贡献标准库代码;比如 StackSet 之类的数据结构。

MapString

以这个 MapString 为例:键值对都为 string 的 HashMap

int count =100;
MapString m1 = MapString();
for (int i=0;i<count;i++){
 string key = i+"";
 string value = key;
 m1.put(key,value);
}
println(m1.getSize());
assertEqual(m1.getSize(),count);

for (int i=0;i<count;i++){
 string key = i+"";
 string value = m1.get(key);
 println("key="+key+ ":"+ value);
 assertEqual(key,value);
}

使用起来和 Java 的 HashMap 类似,当然他的实现源码也是参考的 jdk1.7 的 HashMap

由于目前并有一个类似于 Java 的 object 或者是 go 中的 interface{}, 所以如果需要存放 int,那还得实现一个 MapInt,不过这个通用类型很快会实现。

内置函数

int[] a={1,2,3};
// len 返回数组大小
println(len(a));

// 向数组追加数据
a = append(a,4);
println(a);
// output: [1,2,3,4]

// 断言函数,不相等时会抛出运行时异常,并中断程序。
assertEqual(len(a),4);

// 返回 hashcode
int hashcode = hash(key);

也内置了一些基本函数,当然也这不是由 GScript 源码实现的,而是编译器实现的,所以新增起来要稍微麻烦一些;后续会逐步完善,比如和 IO 相关的内置函数。

最新特性

any类型

首先是 any 通用类型,这个类似于 Java 中的 Object 和 Go 中的 interface{},极大的方便了我们编写一些标准库。

73f100ff4b3ba6306361e7ec418d8760.jpeg

以之前内置的 hash 和 len 函数为例,需要对每种类型都实现一遍,非常麻烦而且毫无必要;现在只需要定义一次即可,代码量直接省几倍。

b02cf73dc59fe26c90cdb5b41f8bedb2.jpeg同理,之前实现的 Map 只支持存放 string 类型,现在便能存放任何类型的数据。

对 any 的实现过程感兴趣的朋友,今后可以单独分享一下。

运算符重载

写 go 或者是 Java 的朋友应该知道,这两门语言都无法对两个对象进行运算,编译器会直接报错。

但在一些特殊场景下还是蛮好用的,于是我参考了 C# 的语法在 GScript 中也实现了。

class Person{
 int age;
 Person(int a){
  age = a;
 }
}
Person operator + (Person p1, Person p2){
 Person pp = Person(p1.age+p2.age);
 return pp;
}
Person operator - (Person p1, Person p2){
 Person pp = Person(p1.age-p2.age);
 return pp;
}
Person p1 = Person(10);
Person p2 = Person(20);
Person p3 = p1+p2;
println("p3.age="+p3.age);
assertEqual(p3.age, 30);

声明的函数名称必须为 operator,之后跟上运算符便实现了重载。

支持的运算符有:+-*/ < >= <= > ==

JSON支持

当前版本中支持将对象、基本类型进行序列化,暂不支持反序列化为对象,但可以根据 JSON 字符串通过一定的语法查询数据。

内置了两个 JSON 相关函数:

// return JSON string
string JSON(any a){}
// JSON query with path
any JSONGet(string json, string path){}
class Person{
 int age;
 string name;
 float weight;
 bool man;
 Person(string n, int a, float w, bool m){
  name = n;
  age = a;
  weight = w;
  man =m;
 }
}
Person p1 = Person("abc",10,99.99,true);
Person p2 = Person("a",11,999.99,false);
string json = JSON(p1);
println(json);
// output:{"age":10,"man":true,"name":"abc","weight":99.99}

以这段代码为例,调用 JSON 函数可以将对象序列化为 JSON 字符串。


class Person{
 int age;
 string name;
 float weight;
 bool man;
 Person(string n, int a, float w, bool m){
  name = n;
  age = a;
  weight = w;
  man =m;
 }
}
Person p1 = Person("abc",10,99.99,true);
string json = JSON(p1);
println(json);

int age = JSONGet(json, "age");
println(age);
assertEqual(age,10);

使用 JSONGet 函数可以在一个 JSON 字符串中查询任意的数据,这个功能是通过适配 XJSON 实现的,所以 XJSON 支持的查询语法都能实现。

string j=^{"age":10, "abc":{"def":"def"},"list":[1,2,3]}^;
String def = JSONGet(j, "abc.def");
println(def);
assertEqual(def,"def");
int l1 = JSONGet(j, "list[0]");
println(l1);
assertEqual(l1,1);

string str=^
{
    "name": "bob",
    "age": 20,
    "skill": {
        "lang": [
            {
                "go": {
                    "feature": [
                        "goroutine",
                        "channel",
                        "simple",
                        true
                    ]
                }
            }
        ]
    }
}
^;
String g = JSONGet(str, "skill.lang[0].go.feature[0]");
println(g);
assertEqual(g,"goroutine");

比如这样复杂的嵌套 JSON,也能通过查询语法获取数据。

HTTP 包

HTTP 包是本次升级的重点,标准库中提供了以下函数和类:

// http lib
// Response json
FprintfJSON(int code, string path, string json){}
// Resonse html
FprintfHTML(int code, string path, string html){}

// path (relative paths may omit leading slash)
string QueryPath(string path){}

string FormValue(string path, string key){}
class HttpContext{
    string path;
    JSON(int code, any v){
        string json = JSON(v);
        FprintfJSON(code, path, json);
    }
    HTML(int code, any v) {
        string html = v;
        FprintfHTML(code, path, html);
    }
    string queryPath() {
        string p = QueryPath(path);
        return p;
    }

    string formValue(string key){
        string v = FormValue(path, key);
        return v;
    }
}
// Bind route
httpHandle(string method, string path, func (HttpContext) handle){
    // println("path="+path);
    HttpContext ctx = HttpContext();
    handle(ctx);
}
// Run http server.
httpRun(string addr){}

具体的使用流程:

  1. 通过定义一个函数变量实现自己的业务逻辑。

  2. 注册路由。

  3. 启动 HTTP 服务。

在自己的 handle 中可以通过 HttpContext 对象拿到请求上下文,可以获取请求参数以及响应数据。具体使用示例可以参考这份代码。de890c5bc8f4ab4d5441b640d12488e2.jpeg

总结

现阶段的 GScript 还有许多功能没有完善,比如 更完善的语法检查、编译报错信息等;现在拿来刷刷 LeetCode 还是没有问题的。

刷题源码:https://github.com/crossoverJie/gscript/tree/main/example/leetcode

目前还有一个问题是没有集成开发环境,现在的开发体验和白板上写代码相差无异,所以后续有时间的话尝试写一个 VS Code 的插件,至少能有语法高亮与提示。

之后抽空再把 SQL 标准库实现了,这样就能愉快的 CURD了。

最后对 GScript 或者是编译原理感兴趣的小伙伴可以加我微信一起交流。

项目源码:https://github.com/crossoverJie/gscript

下载地址:https://github.com/crossoverJie/gscript/releases/tag/v0.0.8 

(完)

点击下方图片,查看更多精彩

92f36b2ab29c6e2b3dadd4e0bdd1ea96.png

03c4f2e28b52842de0b6d3975379239b.png

7aecdb6eef7be2b20aaea350bf51a01c.png

fd9b2bf0d64b5ae2daddf0b7058352d9.png

6f33b80d0c33d892f29c487c21ac5401.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值