JM Tech Power

jm-tech-power-official/JMLang

A programming language made by JM Tech Power

Public Uncategorized
Clone jm-tech-power-official/jmlang.jmproj
README.md
# JMLang

JMLang 是一门用 C++ 从零手写的脚本语言,源文件后缀是 `.jml`。
解释器是一个树遍历解释器(tree-walking interpreter),整个实现就在一个 `interp.cpp` 里。

它支持:

- 数字、字符串、数组、全局变量
- `if / else`、`while`、`break / continue`
- 函数、递归、返回值
- 文件读写
- 结构体、类、继承、多态
- 指针(还能抓野指针)
- 库和 `import`,可以从 jmtechpower.com 在线下载库
- 带文件名和行号的报错信息
- 字符串转义:`\n` `\t` `\"` `\\`

```
func fib n
    if $n < 2
        ret $n
    end
    call fib $n - 1
    set a $ret
    call fib $n - 2
    ret $a + $ret
end

call fib 20
out "fib(20) = " + $ret + "\n"
```

---

## 目录

- [编译和运行](#编译和运行)
- [基本规则](#基本规则)
- [变量和类型](#变量和类型)
- [运算符](#运算符)
- [输入输出](#输入输出)
- [条件和循环](#条件和循环)
- [数组](#数组)
- [字符串](#字符串)
- [函数](#函数)
- [文件读写](#文件读写)
- [结构体](#结构体)
- [类](#类)
- [指针](#指针)
- [库和 import](#库和-import)
- [标准库](#标准库)
- [报错信息](#报错信息)
- [常见的坑](#常见的坑)

---

## 编译和运行

需要 g++ 和 libcurl 的开发包(用来在线下载库)。

```bash
# Ubuntu / Debian
sudo apt install g++ libcurl4-openssl-dev

# 编译
g++ -O2 -o interp interp.cpp -lcurl

# 运行
./interp hello.jml
```

不带参数运行 `./interp` 的话,它会等你输入文件名。

---

## 基本规则

1. **一行一条语句。**
2. **所有东西都用空格隔开**,包括运算符:`set x $a + 1` ✅,`set x $a+1` ❌
3. `#` 开头的行是注释。**注释必须单独占一行**,不能写在代码后面。
4. 缩进随意,只是为了好看。
5. 代码块(`if`、`while`、`func`、`class`)都用 `end` 结尾。
6. **`out` 不会自动换行**,要换行就在字符串里写 `\n`。

```
# 这是注释
out "Hello, JMLang!\n"
```

---

## 变量和类型

| 写法 | 意思 |
|---|---|
| `set x 5` | 给变量 x 赋值 |
| `$x` | 读变量 x 的值 |
| `@x` / `$@x` | 全局变量(在函数里面也能直接用) |

JMLang 有三种值:

- **数字**:整数,比如 `42`、`-7`
- **字符串**:用双引号,比如 `"hello"`
- **对象 / 指针**:用 `new` 和 `&` 得到(后面会讲)

```
set name "Joshua"
set age 10
out $name + " is " + $age + "\n"
```

只要 `+` 两边有一边是字符串,就是**拼接**;两边都是数字,才是加法。

```
# 输出 12
out "1" + 2 + "\n"
# 输出 3
out 1 + 2 + "\n"
```

读一个从来没 `set` 过的变量会报错:`Variable not set`。

---

## 运算符

从**低**到**高**优先级:

| 优先级 | 运算符 | 说明 |
|---|---|---|
| 1 | `or` | 或(短路) |
| 2 | `and` | 且(短路) |
| 3 | `not` | 非 |
| 4 | `<` `<=` `>` `>=` `==` `!=` | 比较,结果是 1 或 0 |
| 5 | `+` `-` | 加减 / 字符串拼接 |
| 6 | `*` `/` `%` | 乘、整除、取余 |
| 7 | `len` `ord` `chr` `read` `lines` `exists` | 一元运算 |
| 8 | `at` `line` | 取字符 / 取文件某行 |

同优先级从左往右算:`10 - 3 - 2` 是 `5`。

没有括号。想改变计算顺序的话,就先算一部分存进变量:

```
set t $a + $b
set r $t * 2
```

真假的规则:数字 `0` 是假,其他数字是真;空字符串 `""` 是假,其他字符串是真;对象和指针永远是真。

---

## 输入输出

| 写法 | 作用 |
|---|---|
| `out 表达式` | 输出(**不会自动换行**,要换行自己加 `"\n"`) |
| `-$in$-` | 读一个词(像数字就当数字,不像就当字符串) |
| `-$time$-` | 现在的 Unix 时间戳(秒) |
| `-$tick$-` | 程序启动以来过了多少毫秒 |

```
out "What's your name? "
set n -$in$-
out "Hi, " + $n + "\n"
```

`out` 不换行的好处是可以把好几次输出拼在同一行,比如上面的提问和你输入的名字就在同一行。

---

## 条件和循环

### if / else

```
if $score >= 60
    out "pass\n"
else
    out "fail\n"
end
```

想写 else-if,就在 `else` 里面再套一个 `if`。

### while

```
set i 0
while $i < 5
    out $i + "\n"
    set i $i + 1
end
```

### break / continue

```
set i 0
while $i < 10
    set i $i + 1
    if $i % 2 == 0
        # 跳过偶数
        continue
    end
    if $i > 7
        # 到 9 就停
        break
    end
    out $i + "\n"
end
# 输出 1 3 5 7(每个一行)
```

`break` 和 `continue` 只作用于**最里面那层** `while`,不会穿过函数。

### 标签和跳转(老派写法)

```
set i 0
:loop
out $i + "\n"
set i $i + 1
jumpif $i < 3 loop
```

`:名字` 定义一个标签,`jump 标签` 无条件跳过去,`jumpif 条件 标签` 条件为真才跳。

---

## 数组

数组元素写成 `名字<下标>`,下标可以是任何表达式(也可以是变量)。

```
set i 0
while $i < 5
    set sq<$i> $i * $i
    set i $i + 1
end
# 输出 9
out $sq<3> + "\n"
```

没设过的数组元素当成 `0`,所以拿来计数很方便:

```
set cnt<$x> $cnt<$x> + 1
```

---

## 字符串

| 写法 | 结果 | 说明 |
|---|---|---|
| `len "hello"` | `5` | 长度 |
| `"hello" at 1` | `"e"` | 第 1 个字符(从 0 开始),越界返回 `""` |
| `ord "A"` | `65` | 字符 → 编码 |
| `chr 65` | `"A"` | 编码 → 字符 |
| `chr 10` | 换行符 | 跟 `"\n"` 一样 |

```
set s "hello"
set r ""
set i 0
while $i < len $s
    set r $s at $i + $r
    set i $i + 1
end
# 输出 olleh
out $r + "\n"
```

### 转义字符

字符串里可以用反斜杠 `\` 写一些特殊字符:

| 写法 | 意思 |
|---|---|
| `\n` | 换行 |
| `\t` | Tab |
| `\"` | 字符串里的双引号 |
| `\\` | 反斜杠本身 |

```
out "Hello\n"
out "a\tb\n"
out "She said \"hi\"\n"
out "C:\\Users\n"
```

输出:

```text
Hello
a	b
She said "hi"
C:\Users
```

更多字符串函数见 [string.jml](#stringjml)。

---

## 函数

```
func add a b
    ret $a + $b
end

call add 3 , 4
# 输出 7
out $ret + "\n"
```

- `func 名字 参数1 参数2 ...` 定义函数,`end` 结尾。
- `call 名字 参数1 , 参数2 , ...` 调用函数,**参数之间用 ` , ` 隔开**。
- `ret 表达式` 返回,返回值放在调用方的 `$ret` 里。不写 `ret` 的话返回 `0`。
- 每次调用都有自己的局部变量,所以递归没问题。
- 要用全局变量就写 `@名字`。

⚠️ `$ret` 每次 `call` 都会被覆盖,要用就马上存起来:

```
call add 1 , 2
set x $ret
call add 10 , 20
# 输出 33
out $x + $ret + "\n"
```

---

## 文件读写

| 写法 | 作用 |
|---|---|
| `write "a.txt" , 表达式` | 清空文件,写一行 |
| `append "a.txt" , 表达式` | 在文件末尾加一行 |
| `read "a.txt"` | 整个文件的内容(字符串) |
| `lines "a.txt"` | 文件有几行 |
| `"a.txt" line 2` | 第 2 行(从 0 开始),内容像数字就当数字 |
| `exists "a.txt"` | 文件在不在(1 / 0) |

```
write "save.txt" , "hello"
append "save.txt" , 42
set i 0
while $i < lines "save.txt"
    out $i + ": " + "save.txt" line $i + "\n"
    set i $i + 1
end
```

文件不存在的时候:`read` 返回 `""`,`lines` 返回 `0`,读不到的行返回 `""`。

---

## 结构体

```
struct Point x y

new p Point 3 , 4
# 输出 3,4
out $p.x + "," + $p.y + "\n"
set p.x 10
```

- `struct 名字 字段1 字段2 ...` 定义结构体(只有一行)。
- `new 变量 类型 参数...` 创建,参数按顺序填进字段,没给的字段是 `0`。
- 用 `.` 读写字段,可以一层层套:`$line.start.x`。

### 对象是引用

```
set q $p
set q.y 99
# 输出 99,因为 p 和 q 是同一个对象
out $p.y + "\n"
```

对象也可以放进数组、传进函数:

```
new pts<0> Point 1 , 2
# 输出 2
out $pts<0>.y + "\n"
```

---

## 类

```
class Counter
    # new 的时候自动调用
    func init start
        set self.n $start
    end
    func add k
        set self.n $self.n + $k
    end
    func get
        ret $self.n
    end
end

new c Counter 10
call $c.add 5
call $c.get
# 输出 15
out $ret + "\n"
```

- 方法里用 `self` 表示"这个对象自己"。
- 调用方法要写 `call $对象.方法 参数...`。**要带 `$`**,用来跟普通函数 `call math.max` 区分开。
- 方法内部的名字是 `类名.方法名`,比如 `Counter.add`。

### 继承和多态

```
class Animal
    func init name
        set self.name $name
    end
    func speak
        out $self.name + " makes a sound\n"
    end
    func intro
        out "I am " + $self.name + "\n"
        call $self.speak
    end
end

class Dog extends Animal
    func speak
        out $self.name + " says woof\n"
    end
end

new d Dog "Rex"
call $d.intro
# I am Rex
# Rex says woof
```

`Dog` 自己没有 `intro`,所以用父类 `Animal` 的;但 `intro` 里调用 `speak` 的时候,跑的是 `Dog` 自己的版本。这就是多态。

---

## 指针

| 写法 | 意思 |
|---|---|
| `&x` | 取 x 的地址 |
| `$*p` | 读 p 指向的值 |
| `set *p 值` | 通过 p 写 |
| `$p + 1` | 指向下一个数组元素 |

指针可以指向普通变量、全局变量、数组元素、对象字段,也可以指向另一个指针。

```
func swap a b
    set t $*a
    set *a $*b
    set *b $t
end

set m 1
set n 2
call swap &m , &n
# 输出 2 1
out $m + " " + $n + "\n"
```

### 用指针遍历数组

```
set i 0
while $i < 5
    set arr<$i> $i * 10
    set i $i + 1
end

set q &arr<0>
set stop &arr<5>
while $q != $stop
    out $*q + "\n"
    set q $q + 1
end
```

### 指针的指针

```
set x 1
set p &x
set pp &p
set **pp 7
# 输出 7
out $x + "\n"
```

### 野指针检测

```
func bad
    set local 5
    ret &local
end
call bad
out $*ret + "\n"
```

```text
[bad.jml Line 6] Dangling pointer: local is gone (its function already returned)
```

函数返回之后,它的局部变量就没了。C 里继续用这种指针会读到乱七八糟的东西;JMLang 会直接报错告诉你。

空指针可以用 `0` 表示,对它解引用会报 `Not a pointer`。

---

## 库和 import

库就是普通的 `.jml` 文件,里面写函数和类。

```
import "math.jml"
call math.max 3 , 9
out $ret + "\n"
```

| 写法 | 行为 |
|---|---|
| `import "x.jml"` | 先在本地找,找不到就从 jmtechpower.com 下载 |
| `import "jm://x.jml"` | 每次都从 jmtechpower.com 重新下载(会覆盖本地文件) |

- 同一个文件只会被导入一次,所以 A 导入 B、B 又导入 A 也不会死循环。
- 库可以 import 别的库,比如 `random.jml` 导入了 `time.jml`,`file.jml` 导入了 `string.jml`。
- 惯例:库里的函数都用 `库名.函数名` 命名,全局变量用 `@库名.变量名`,避免撞名。

在线库放在 <https://www.jmtechpower.com/jm-tech-power-official/jmlang>,想分享自己的库,就把文件传到那个仓库里。

---

## 标准库

全部都是用 JMLang 自己写的,没有一行 C++。

### math.jml

| 函数 | 作用 |
|---|---|
| `math.max a , b` | 较大值 |
| `math.min a , b` | 较小值 |
| `math.abs x` | 绝对值 |
| `math.pow a , b` | a 的 b 次方 |
| `math.gcd a , b` | 最大公约数 |

### random.jml

| 函数 | 作用 |
|---|---|
| `random.seed s` | 设置种子(导入的时候会自动用当前时间做种子) |
| `random.next` | 下一个随机数 |
| `random.int lo , hi` | `lo` 到 `hi` 之间的随机整数 |

### time.jml

| 函数 | 作用 |
|---|---|
| `time.now` | Unix 时间戳 |
| `time.clock` | `"HH:MM:SS"` |
| `time.date` | `"YYYY-MM-DD"` |
| `time.sleep ms` | 等待几毫秒 |
| `time.start` / `time.elapsed` | 计时器 |

时区由 `@time.zone` 控制,默认是 `8`(北京时间)。

### string.jml

| 函数 | 作用 |
|---|---|
| `string.reverse s` | 反转 |
| `string.upper s` / `string.lower s` | 转大写 / 小写 |
| `string.sub s , start , n` | 从 start 开始取 n 个字符 |
| `string.find s , sub` | sub 第一次出现的位置,找不到返回 `-1` |
| `string.repeat s , n` | 重复 n 次 |

### file.jml

| 函数 | 作用 |
|---|---|
| `file.print path` | 带行号打印整个文件 |
| `file.copy src , dst` | 复制文件,返回复制了几行 |
| `file.find path , text` | 第一个包含 text 的行号,没有就返回 `-1` |
| `file.count path , word` | word 在文件里一共出现几次 |

---

## 报错信息

出错的时候,JMLang 会告诉你**哪个文件、第几行、那一行写的是什么**,包括库文件里的错误:

```text
[game.jml Line 12] Variable not set: $scroe
    > out $scroe + 1
```

| 报错 | 意思 |
|---|---|
| `Variable not set` | 用了没赋过值的变量(多半是拼错了) |
| `Divide by 0` | 除以 0 或者对 0 取余 |
| `String "..." can't do math` | 字符串做了 `-` `*` `/` 之类的运算 |
| `Code not complete` | 运算符旁边少了东西,比如 `set x 1 +` |
| `Can't understand: ...` | 多了东西,比如 `set x 5 6` |
| `Block has no end` | `if` / `while` / `func` / `class` 少了 `end` |
| `Extra end` | 多了一个 `end` |
| `else without if` | `else` 前面没有对应的 `if` |
| `break not in while` | `break` / `continue` 不在循环里 |
| `Function not found` | 函数名写错了,或者忘了 import |
| `Label not found` | `jump` 的标签不存在 |
| `Type not found` | `new` 了一个没定义的结构体 / 类 |
| `No field: ...` | 对象没有这个字段 |
| `... has no method: ...` | 类没有这个方法 |
| `Not an object` | 对不是对象的东西用了 `.` |
| `Not a pointer` | 对不是指针的东西用了 `*` |
| `Dangling pointer` | 指针指向的局部变量已经没了 |
| `Pointer math only works on array elements` | 只有指向数组元素的指针才能 `+ 1` |

---

## 常见的坑

- **`out` 不会自动换行。** 忘了加 `"\n"` 的话,好几次输出会挤在同一行。
- **拼接是从左往右算的。** `out $a + $b + "\n"` 先算数字加法再拼接;但 `out "\n" + $a + $b` 一开始就变成了字符串,`$a` 和 `$b` 会被拼在一起而不是相加。
- **符号两边要有空格。** `$a+1` 会被当成一个变量名。
- **没有括号。** 复杂的式子拆成几行写。
- **`at` 和 `line` 优先级很高。** `"a.txt" line $i + 1` 的意思是 `("a.txt" line $i) + 1`,想读第 i+1 行要先 `set j $i + 1`。
- **`$ret` 会被下一次 `call` 覆盖。** 用之前先存起来。
- **只有整数。** `7 / 2` 是 `3`。
- **`new` 只能单独写一行**,不能放在表达式里。
- **对象和指针用完不会被回收**,小程序没关系。
- **`$*p.x` 暂时不支持**,先 `set o $*p`,再用 `$o.x`。
- **子类没有 `super`**,调不到父类被覆盖掉的方法。

---

## 一个完整的例子:带最高纪录的猜数字

```
import "random.jml"

set best 9999
if exists "best.txt"
    set best "best.txt" line 0
end
out "Best so far: " + $best + "\n"

call random.int 1 , 100
set ans $ret
set tries 0
set g 0
while $g != $ans
    out "Guess (1-100): "
    set g -$in$-
    set tries $tries + 1
    if $g < $ans
        out "bigger\n"
    else
        if $g > $ans
            out "smaller\n"
        end
    end
end

out "Got it in " + $tries + " tries!\n"
if $tries < $best
    write "best.txt" , $tries
    out "New record!\n"
end
```

---

Made by Joshua · <https://www.jmtechpower.com>