Compare commits

...

41 Commits

Author SHA1 Message Date
PuqiAR 68e6552d07 Rewrite: C++ → Rust, Lexer + Error system complete
全量 Rust 重写 Fig 编译器前端。

Lexer: 43 个测试,千行压力测试通过,完整 UTF-8 支持(中文标识符/字符串),运算符最长匹配打表,单行/多行注释 + 错误恢复。

Error System: Diagnostic trait + Warning/Error/Critical 三级严重度,源码上下文渲染(Unicode 波浪线、中英双语 i18n),SourcePosition/SourceRange 为 LSP 准备,Related 关联信息,thrower 宏追踪编译器源码位置。

Build: cargo + build.rs(git hash + 编译时间戳)。

删除全部 C++ 源码。
2026-07-23 22:50:37 +08:00
PuqiAR 82c7c7178f Refactor: 更新OpCode枚举,添加详细注释 2026-07-23 13:01:56 +08:00
PuqiAR 9f7fe79a2e Made some preparations to refactor semantic analysis and type system 2026-07-21 22:52:17 +08:00
PuqiAR 0f5304001c 优化注释 2026-07-04 19:32:01 +08:00
PuqiAR 680197aafe Refactor: 重构Parser和AST结构,以支持新的语言特性
- 更新了 ParserTest,以改进文件路径处理和输出格式。
- 在 StmtParser 中新增了 parseConstDecl 和 parseForStmt 方法,用于处理常量声明和 for 循环。
- TypeExpr现归类为Expr。TypeExpr属于Expr,语义阶段视为Expr
- 添加了新的 AST 节点:PostfixExpr、TernaryExpr、ForStmt 和 ImportStmt,用于表示新的语法结构。
2026-06-06 22:12:04 +08:00
PuqiAR 4f87078a87 Plan 2026-06-01 18:52:06 +08:00
PuqiAR 9338c21449 *(无用) feat: 在VM.cpp中添加likely属性以优化分支预测
refact: 在xmake.lua中优化构建设置
2026-05-10 21:25:05 +08:00
PuqiAR 98de782760 feat: 增加repl入口,-r/--repl。 添加计时选项。 -- 我发现一个问题,analyzer没法保存环境。完了。 2026-04-30 21:24:11 +08:00
PuqiAR fafa2b4946 feat: 在解析器中实现 Lambda 和 new 表达式
- 增加了对 Lambda 表达式的初步解析支持,包括参数处理和返回类型。Lambda闭包尚未支持。
- 引入了用于对象初始化的新的表达式,支持可选的命名参数。
- 改进了表达式语法错误的错误报告。
- 更新了解析器和分析器以处理新的表达式类型并验证其语义。
- 修改了现有测试以涵盖新功能并确保其正确性。
- 改进了各种解析和语义错误的诊断。
2026-04-12 10:07:51 +08:00
PuqiAR 570a87c3cd feat: 增加函数类型表达式支持,更新解析器和分析器 2026-03-18 17:30:09 +08:00
PuqiAR e1d9812f92 refact:实现参数解析器和入口点
- 新增了一个名为 ArgumentParser 的类来处理命令行参数,其中包括用于显示帮助、显示版本和显示许可证的标志。
- 更新了 main.cpp 以使用 ArgumentParser 来改进命令行界面。
- 创建了 Entry.cpp 和 Entry.hpp 来封装虚拟机执行逻辑,从而实现更好的关注点分离。
- 调整了 xmake.lua 以包含 ArgumentParser 和 Entry 组件的新源文件。
- 强化了命令行使用的错误处理和用户反馈。
2026-03-14 14:18:21 +08:00
PuqiAR 6bcc98bdb3 删除无用的东西233 2026-03-13 20:11:46 +08:00
PuqiAR 91b5a0e384 feat: 增加一个很简单的repl(还不能用) 写了readme 2026-03-13 01:28:43 +08:00
PuqiAR c0eacfd236 refactor: 修改Disassembler使用CoreIO的stream 2026-03-11 21:51:58 +08:00
PuqiAR 51a939ac45 对编译器和虚拟机进行重构,以支持闭包和垃圾回收功能
- 去除了不再使用的结构,并更新了编译器以处理新的闭包语义。
- 改进了 Compiler,使其能够生成带有源位置跟踪的指令。
- 在 FunctionObject 和 VM 中引入了作用域变量管理,以支持动态闭包。
- 实现了使用标记-扫描(Mark-And-Sweep) (Tri-Color tracing) 算法的垃圾回收机制,包括对作用域变量的处理。
- 在 VM 中增加了函数加载和作用域变量检索的支持。
- 更新了对象模型,包括引入 InstanceObject 并改进内存管理。
- 添加了用于调试的全局变量打印功能。
2026-03-11 16:53:10 +08:00
PuqiAR 0f635ccf2b 重构类型系统并改进诊断功能
- 更新了类型系统,新增了类型并优化了结构。
- 引入了基类型和派生类,用于函数、结构体和接口类型。
- 实现了类型上下文,用于管理内置类型和类型解析。
- 添加了诊断类,用于收集和报告警告和错误。
- 通过改进错误处理增强了虚拟机执行,以应对递归限制问题。
- 实现了反汇编器,将字节码转换为代码,以改善调试和分析。
- 添加了新的抽象语法树节点,用于成员表达式、对象初始化、接口和结构体定义。
- 引入了语义错误测试,包括重定义、未声明的变量和无效的结构字段。
2026-03-10 12:33:17 +08:00
PuqiAR 90448006ff refactor: 引入 Arena 内存池并优化指令分发,为类型系统重构做准备 2026-03-08 15:59:55 +08:00
PuqiAR 91e4eb734e feat: 使用Computed Goto优化指令分发机制和算术运算处理 2026-03-07 21:33:55 +08:00
PuqiAR 6dbecbbdc0 feat: 重构编译器以支持函数定义和调用,添加新的字节码以支持函数调用
另外,我很高兴地宣布,fib(40) 递归法 在我的平台, i5-13490f,只需要 6600ms, fib(30) 56ms
这是历史性的一刻!
2026-03-07 00:34:52 +08:00
PuqiAR 1fe9ccf7ea feat: 实现控制流语句并优化类型解析
- 新增了 ReturnStmt、BreakStmt 和 ContinueStmt 结构,以支持 AST 中的控制流。
- 引入了 TypeInfo 和 TypeContext 以实现更好的类型管理和解析。
- 对分析器进行了增强,能够处理函数定义和返回语句,包括类型检查和错误处理。
- 更新了编译器和解析器以适应新的控制流语句和类型解析逻辑。
- 重构了现有代码以提高清晰度和可维护性,包括对表达式和语句中的类型处理的更改。
- 删除了过时的 String 和 Struct 定义,代之以 StringObject 和 StructObject 以实现更好的对象表示。
2026-02-28 20:42:15 +08:00
PuqiAR bb23ddf9fa feat: 添加函数定义和类型表达式支持,重构解析器以处理新语法(函数定义) 2026-02-26 14:41:41 +08:00
PuqiAR 12dc31a6c0 fix: 修复了在解析字面量时的条件逻辑,确保整数转换在浮点数转换之后进行 2026-02-25 19:15:25 +08:00
PuqiAR a0fb8cdffb feat: 添加“while 语句”支持,并对解析器进行重构以处理控制流相关内容
- 引入了 WhileStmt 结构来表示 while 循环语句。
- 在解析器中实现了对 while 语句的解析逻辑。
- 在分析器中为 while 语句添加了语义分析。
- 重构了现有的解析器方法,以利用 StateProtector 进行状态管理。
- 更新了对各种表达式和语句的错误处理。
- 移除了未使用的终止符管理方法,并简化了表达式解析。
- 将 FigLSPServer.cpp 重命名为 LSPServer.cpp,并调整了构建配置。
- 增强了重复声明和类型错误的错误报告。
- 在多个文件中改进了代码格式和一致性。
2026-02-25 17:31:00 +08:00
PuqiAR b7bb889676 feat: 增加了analyzer, compiler不再分析并且报错, 只生产 bytecode, analyzer作为前端结束最后一道防线检查代码,同时引入一个简单的LSP 2026-02-23 19:57:28 +08:00
PuqiAR 852dd27836 feat: 优化表达式解析器,添加终止符重置功能及代码格式调整 2026-02-23 15:03:53 +08:00
PuqiAR abdb1d2fb0 feat: 添加跳转指令支持及条件语句编译实现 2026-02-20 17:02:13 +08:00
PuqiAR eb20993e27 feat: 添加If语句及块语句解析支持 2026-02-20 15:46:33 +08:00
PuqiAR 2631f76da1 feat: Implement compiler and virtual machine for Fig language
- Added Compiler class with methods for compiling programs, statements, and expressions.
- Introduced Proto structure to hold compiled bytecode and constants.
- Implemented expression compilation including literals, identifiers, and infix expressions.
- Developed statement compilation for variable declarations and expression statements.
- Created a VM class to execute compiled bytecode with support for arithmetic and comparison operations.
- Added Object and Value classes for handling different data types and memory management.
- Implemented String and Struct objects for enhanced data representation.
- Established a parser for parsing variable declarations and statements.
- Included tests for the VM and object representations.
2026-02-20 14:05:56 +08:00
PuqiAR f2e899c7a7 更换新Logo! 然后为拓展增加Logo。 2026-02-18 14:28:19 +08:00
PuqiAR c81da16dfb 修复EOF飘逸(去除末尾\n)以及其他修复... 2026-02-18 00:16:59 +08:00
PuqiAR 663fe39070 添加缺失的 doxy 2026-02-17 14:13:57 +08:00
PuqiAR 6b75e028ff 修复右结合绑定力错误 2026-02-17 14:03:48 +08:00
PuqiAR 878157c2fc 完成Parser定义以及表达式解析 2026-02-14 23:03:46 +08:00
PuqiAR 35e479fd05 完成表达式Ast定义。修改了format文件 2026-02-14 18:00:46 +08:00
PuqiAR 51e831cc6a 添加了注释文档 2026-02-14 15:32:11 +08:00
PuqiAR 35b98c4d7f 完成Lexer实现,100%可靠 2026-02-14 14:54:44 +08:00
PuqiAR 877253cbbc 完成 Error定义和ErrorLog. 以及一些相关的东西 2026-02-13 23:11:37 +08:00
PuqiAR cfcdfde170 结构调整2 2026-02-12 14:55:48 +08:00
PuqiAR 5e75402b43 项目结构调整 2026-02-12 14:55:34 +08:00
PuqiAR 642ce66f75 Merge branch 'main' of https://git.fig-lang.cn/PuqiAR/Fig 2026-02-12 14:54:49 +08:00
PuqiAR 58212a3715 初始化仓库,包含format文件,ci, actions和文档。新的路开始了! 2026-02-12 14:51:26 +08:00
67 changed files with 4744 additions and 14 deletions
+15
View File
@@ -0,0 +1,15 @@
# Xmake cache
.xmake/
build/
# MacOS Cache
.DS_Store
.vscode
.VSCodeCounter
/test.fig
# Added by cargo
/target
Generated
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "fig"
version = "0.6.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "fig"
version = "0.6.0"
edition = "2021"
[dependencies]
+79
View File
@@ -0,0 +1,79 @@
var any_var; // type is Any
var number = 10; // type is Int
number = "123"; // valid
var number2 := 10; // specific type is Int
var number3: Int = 10; // both is ok
/*
number2 = 3.14;
invalid!
*/
const Pi := 3.14; // recommended, auto detect type
// equal -> const Pi: Double = 3.14;
/*
In fig, we have 13 builtin-type
01 Any
02 Null
03 Int
04 String
05 Bool
06 Double
07 Function
08 StructType
09 StructInstance
10 List
11 Map
12 Module
13 InterfaceType
3, 4, 5, 6, 10, 11 are initable
value system:
object is immutable
(included basic types: Int, String...)
`variable` is a name, refers to an object
assignment is to bind name to value
Example: var a := 10;
[name] 'a' ---> variable slot (name, declared type, access modifier, [value) ---> ObjectPtr ---> raw Object class
bind bind (shared_ptr)
For example:
var a := 10;
var b := 10;
`a` and `b` reference to the same object in memory
a = 20;
now a refers to a new object (20, Int)
what about complex types?
they actually have same behaviors with basic types
var a := [1, 2, 3, 4];
var b := a;
> a
[1, 2, 3, 4]
> b
[1, 2, 3, 4]
set a[0] to 5
> a
[5, 2, 3, 4]
> b
[5, 2, 3, 4]
Why did such a result occur?
" `a` and `b` reference to the same object in memory "
If you wish to obtain a copy, use List {a} to deeply copy it
*/
+16
View File
@@ -0,0 +1,16 @@
import std.io;
func greeting(name:String) -> String
{
return "Hello " + name + "!";
}
io.println(greeting("Fig"));
func adder(x)
{
return func (n) => x + n; // closure
}
const add2 = adder(2);
io.println(add2(3));
+30
View File
@@ -0,0 +1,30 @@
import std.io;
struct Point
{
x: Int; // type specifiers are optional
y: Int; // type specifiers are optional
// x and y are private fields, can only reached by internal context
public func toString() -> String
{
return "(" + (x as String) + "," + (y as String) + ")";
}
// public func toString() {} is ok
}
// make points
var p1 := new Point{1, 2};
io.println(p1.toString()); // (1,2)
var p2 := new Point{x: 2, y: 3};
io.println(p2.toString()); // (2,3)
var x := 114;
var y := 514;
var p3 := new Point{y, x}; // shorthand mode, can be unordered, auto match field and variable!
// = Point{x: x, y: y}
io.println(p3.toString()); // (114,514)
+94
View File
@@ -0,0 +1,94 @@
import std.io;
interface Document
{
getDepth() -> Int; // return type is necessary
getName() -> String;
/* toString() -> String
{
// default implementation
}
*/
}
struct File
{
public depth: Int;
public name: String;
}
impl Document for File
{
getDepth()
{
return depth;
}
getName()
{
return name;
}
}
struct Folder
{
public depth: Int;
public name: String;
public childs: List = [];
}
impl Document for Folder
{
getDepth()
{
return depth;
}
getName()
{
return name;
}
}
const root_folder := new Folder{
0,
"root",
[
new File{
1,
"joyo.txt"
},
new Folder{
2,
"joyoyo",
[
new File{
3,
"JOYO2.txt"
},
new Folder{
3,
"joyoyoyo"
},
]
}
]
};
func print_directory(root: Document)
{
io.print(" " * root.getDepth());
io.println(root.getDepth(), root.getName());
if root is Folder
{
for var i := 0; i < root.childs.length(); i += 1
{
var child := root.childs[i];
print_directory(child);
}
}
}
print_directory(root_folder);
+15
View File
@@ -0,0 +1,15 @@
import std.io;
import token {Token, TokenType};
import tokenizer {Tokenizer};
const src := "abc egaD";
const tokenizer := new Tokenizer{src};
const result := tokenizer.TokenizeAll();
for var i := 0; i < result.length(); i += 1
{
const tok := result[i];
io.printf("{}: {}\n", tok.literal, tok.type);
}
+31
View File
@@ -0,0 +1,31 @@
/*
Example code: FigFig
Token.fig
Copyright (C) 2020-2026 PuqiAR
*/
struct _TokenTypes
{
public EOF = -1;
public Identifier = 0;
public StringLiteral = 1;
public NumberLiteral = 2;
public True = 3;
public False = 4;
public Null = 5;
public Plus = 6;
public Minus = 7;
public Asterisk = 8;
public Slash = 9;
}
public const TokenType := new _TokenTypes{};
public struct Token
{
public literal: String = "";
public type: Int = TokenType.EOF;
}
+102
View File
@@ -0,0 +1,102 @@
/*
Example code: FigFig
Tokenizer.fig
Copyright (C) 2020-2026 PuqiAR
*/
import token {Token, TokenType};
func list_contains(lst: List, value: Any) -> Bool
{
for var i := 0; i < lst.length(); i += 1
{
if lst[i] == value
{
return true;
}
}
return false;
}
func isspace(c: String) -> Bool
{
return c == " " || c == "\n" || c == "\t";
}
func isalpha(c: String) -> Bool
{
const alb := [
"a", "b", "c", "d",
"e", "f", "g", "h",
"i", "j", "k", "l",
"m", "n", "o", "p",
"q", "r", "s", "t",
"u", "v", "w", "x",
"y", "z",
"A", "B", "C", "D",
"E", "F", "G", "H",
"I", "J", "K", "L",
"M", "N", "O", "P",
"Q", "R", "S", "T",
"U", "V", "W", "X",
"Y", "Z"
];
return list_contains(alb, c);
}
public struct Tokenizer
{
src: String = "";
idx: Int = 0;
func next() -> Null
{
idx += 1;
}
func hasNext() -> Bool
{
return idx < src.length();
}
func produce() -> String
{
const tmp := src[idx];
idx += 1;
return tmp;
}
func current() -> String
{
return src[idx];
}
public func TokenizeAll() -> List
{
var output := [];
const push := func (tok: Token) => output.push(tok);
while hasNext()
{
while hasNext() && isspace(current())
{
next();
}
if isalpha(current())
{
var identi := "";
while hasNext() && isalpha(current())
{
identi += produce();
}
push(new Token{identi, TokenType.Identifier});
}
}
return output;
}
}
+13
View File
@@ -0,0 +1,13 @@
import std.io;
func fib(x:Int) -> Int
{
if (x <= 1)
{
return x;
}
return fib(x-1) + fib(x-2);
}
var result := fib(25);
io.println("result: ", result);
+11
View File
@@ -0,0 +1,11 @@
function fib(n)
if (n <= 1) then
return n
else
return fib(n - 1) + fib(n - 2) end
end
local start = os.clock()
local result = fib(30)
local endt = os.clock()
print(result, " cost: ", (endt - start) * 1000, "ms")
+11
View File
@@ -0,0 +1,11 @@
from time import time as tt
def fib(x:int) -> int:
if x <= 1: return x
return fib(x-1) + fib(x-2)
if __name__ == '__main__':
t0 = tt()
result = fib(35)
t1 = tt()
print('cost: ',t1-t0, 'result:', result)
+84
View File
@@ -0,0 +1,84 @@
import std.io;
import std.time;
import std.value;
func benchmark(fn: Function, arg: Any) -> Null
{
io.println("Testing fn:", fn, "with arg:", arg);
const start := time.now();
const result := fn(arg);
const end := time.now();
const duration := new time.Time{
end.since(start)
};
io.println("=" * 50);
io.println("fn returns:", result, "\n");
io.println("Cost:", duration.toSeconds(), "s");
io.println(" ", duration.toMillis(), "ms");
}
var memo := {};
func fib_memo(x)
{
if memo.contains(x)
{
return memo.get(x);
}
if x <= 1
{
memo[x] = x;
return x;
}
var result := fib_memo(x - 1) + fib_memo(x - 2);
memo[x] = result;
return result;
}
func fib_iter(n)
{
var a := 0;
var b := 1;
for var i := 0; i < n; i = i + 1
{
var temp := a + b;
a = b;
b = temp;
}
return a;
}
func fib(x)
{
if x <= 1
{
return x;
}
return fib(x - 1) + fib(x - 2);
}
func fib_tail(n, a=0, b=1) {
if n == 0 { return a; }
if n == 1 { return b; }
return fib_tail(n-1, b, a+b);
}
const n := 30;
io.println("! fib(" + value.string_from(n) + "):");
benchmark(fib, n);
io.print("\n\n");
io.println("! fib_memo(" + value.string_from(n) + "):");
benchmark(fib_memo, n);
io.print("\n\n");
io.println("! fib_iter(" + value.string_from(n) + "):");
benchmark(fib_iter, n);
io.print("\n\n");
io.println("! fib_tail(" + value.string_from(n) + "):");
benchmark(fib_tail, n);
io.print("\n\n");
+27
View File
@@ -0,0 +1,27 @@
import std.io;
import std.value;
var callCnt:Int = 0;
func fib(x:Int) -> Int
{
callCnt = callCnt + 1;
if (x <= 1)
{
return x;
}
return fib(x-1) + fib(x-2);
}
var fibx:Int;
io.print("input an index of fib ");
fibx = value.int_parse(io.read());
var cnt:Int = 0;
io.println("test forever");
while (true)
{
cnt = cnt + 1;
io.println("test ", cnt,",result: ", fib(fibx));
io.println("func `fib` called ", callCnt);
callCnt = 0;
}
+29
View File
@@ -0,0 +1,29 @@
import std.io;
import std.test;
var ascii_string_test := new test.Test{
"ascii_string_test",
func () => io.println("Hello," + " world!"),
2
};
var unicode_string_test := new test.Test{
"unicode_string_test",
func () => io.println("你好," + " 世界!"),
2
};
var unicode_string_inserting_test := new test.Test{
"unicode_string_inserting_test",
func (){
var str := "我是你的粑粑";
str.insert(1, "不");
return str;
},
"我不是你的粑粑"
};
var tests := [ascii_string_test, unicode_string_test, unicode_string_inserting_test];
var tester := new test.Tester{tests};
tester.TestAll();
+5
View File
@@ -0,0 +1,5 @@
out
dist
node_modules
.vscode-test/
*.vsix
+5
View File
@@ -0,0 +1,5 @@
import { defineConfig } from '@vscode/test-cli';
export default defineConfig({
files: 'out/test/**/*.test.js',
});
+11
View File
@@ -0,0 +1,11 @@
.vscode/**
.vscode-test/**
src/**
.gitignore
.yarnrc
vsc-extension-quickstart.md
**/tsconfig.json
**/eslint.config.mjs
**/*.map
**/*.ts
**/.vscode-test.*
+9
View File
@@ -0,0 +1,9 @@
# Change Log
All notable changes to the "fig-vscode" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release
+46
View File
@@ -0,0 +1,46 @@
MIT License (Fig)
Copyright (c) 2025 PuqiAR <im@puqiar.top>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
2. This project includes code from the following projects with their respective licenses:
- argparse (MIT License)
Copyright (c) 2018 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- magic_enum (MIT License)
Copyright (c) 2019 - 2024 Daniil Goncharov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+3
View File
@@ -0,0 +1,3 @@
# fig-vscode README
Fuck.
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
import typescriptEslint from "typescript-eslint";
export default [{
files: ["**/*.ts"],
}, {
plugins: {
"@typescript-eslint": typescriptEslint.plugin,
},
languageOptions: {
parser: typescriptEslint.parser,
ecmaVersion: 2022,
sourceType: "module",
},
rules: {
"@typescript-eslint/naming-convention": ["warn", {
selector: "import",
format: ["camelCase", "PascalCase"],
}],
curly: "warn",
eqeqeq: "warn",
"no-throw-literal": "warn",
semi: "warn",
},
}];
+4
View File
@@ -0,0 +1,4 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" fill="#F28C28"/>
<path d="M135 100 H395 V195 H230 V265 H370 V360 H230 V430 H135 V100 Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 244 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M135 100 H395 V195 H230 V265 H370 V360 H230 V430 H135 V100 Z" fill="#F28C28"/>
</svg>

After

Width:  |  Height:  |  Size: 196 B

+147
View File
@@ -0,0 +1,147 @@
{
"name": "fig-vscode",
"version": "0.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fig-vscode",
"version": "0.5.0",
"dependencies": {
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/mocha": "^10.0.10",
"@types/node": "^20.6.0",
"@types/vscode": "^1.109.0",
"typescript": "^5.2.2"
},
"engines": {
"vscode": "^1.108.0"
}
},
"node_modules/@types/mocha": {
"version": "10.0.10",
"resolved": "https://registry.npmmirror.com/@types/mocha/-/mocha-10.0.10.tgz",
"integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.33",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.33.tgz",
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/vscode": {
"version": "1.109.0",
"resolved": "https://registry.npmmirror.com/@types/vscode/-/vscode-1.109.0.tgz",
"integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==",
"dev": true,
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vscode-languageclient": {
"version": "9.0.1",
"resolved": "https://registry.npmmirror.com/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
"license": "MIT",
"dependencies": {
"minimatch": "^5.1.0",
"semver": "^7.3.7",
"vscode-languageserver-protocol": "3.17.5"
},
"engines": {
"vscode": "^1.82.0"
}
},
"node_modules/vscode-languageclient/node_modules/minimatch": {
"version": "5.1.7",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.7.tgz",
"integrity": "sha512-FjiwU9HaHW6YB3H4a1sFudnv93lvydNjz2lmyUXR6IwKhGI+bgL3SOZrBGn6kvvX2pJvhEkGSGjyTHN47O4rqA==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.5",
"resolved": "https://registry.npmmirror.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"license": "MIT",
"dependencies": {
"vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5"
}
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.5",
"resolved": "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
"license": "MIT"
}
}
}
+62
View File
@@ -0,0 +1,62 @@
{
"name": "fig-vscode",
"displayName": "Fig Language",
"description": "VSCode extension for Fig language with syntax highlighting and lsp support",
"version": "0.5.0",
"publisher": "PuqiAR",
"engines": {
"vscode": "^1.108.0"
},
"categories": [
"Programming Languages"
],
"repository": {
"url": "https://github.com/PuqiAR/Fig"
},
"activationEvents": [
"onLanguage:fig"
],
"main": "./out/extension.js",
"contributes": {
"languages": [
{
"id": "fig",
"aliases": [
"Fig",
"fig"
],
"extensions": [
".fig"
],
"configuration": "./language-configuration.json",
"icon": {
"light": "./images/Logo.svg",
"dark": "./images/LogoDark.svg"
}
}
],
"grammars": [
{
"language": "fig",
"scopeName": "source.fig",
"path": "./syntaxes/fig.tmLanguage.json"
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile",
"test": "node ./out/test/runTest.js"
},
"dependencies": {
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/mocha": "^10.0.10",
"@types/node": "^20.6.0",
"@types/vscode": "^1.108.0",
"typescript": "^5.9.3"
}
}
+44
View File
@@ -0,0 +1,44 @@
import * as path from 'path';
import { ExtensionContext, workspace } from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
ServerOptions
} from 'vscode-languageclient/node';
let client: LanguageClient;
export function activate(context: ExtensionContext) {
const exeName = process.platform === 'win32' ? 'Fig-LSP.exe' : 'Fig-LSP';
// 获取插件安装后的绝对沙箱路径
const serverCommand = context.asAbsolutePath(path.join('bin', exeName));
const serverOptions: ServerOptions = {
run: { command: serverCommand, args: [] },
debug: { command: serverCommand, args: [] }
};
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'fig' }],
synchronize: {
fileEvents: workspace.createFileSystemWatcher('**/*.fig')
}
};
client = new LanguageClient(
'figLanguageServer',
'Fig Language Server',
serverOptions,
clientOptions
);
client.start();
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}
@@ -0,0 +1,25 @@
{
"comments": {
"lineComment": "//",
"blockComment": ["/*", "*/"]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"" },
{ "open": "'", "close": "'" }
],
"surroundingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"" },
{ "open": "'", "close": "'" }
]
}
+15
View File
@@ -0,0 +1,15 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});
+114
View File
@@ -0,0 +1,114 @@
{
"name": "Fig",
"scopeName": "source.fig",
"patterns": [
{ "include": "#comments" },
{ "include": "#strings" },
{ "include": "#numbers" },
{ "include": "#keywords" },
{ "include": "#operators" },
{ "include": "#functions" },
{ "include": "#identifiers" }
],
"repository": {
"comments": {
"patterns": [
{ "name": "comment.line.double-slash.fig", "match": "//.*$" },
{ "name": "comment.block.fig", "begin": "/\\*", "end": "\\*/" }
]
},
"strings": {
"patterns": [
{
"name": "string.quoted.double.fig",
"begin": "\"\"\"",
"end": "\"\"\"",
"patterns": [{ "match": ".", "name": "string.content.fig" }]
},
{
"name": "string.quoted.double.fig",
"begin": "\"",
"end": "\"",
"patterns": [
{ "match": "\\\\.", "name": "constant.character.escape.fig" }
]
},
{
"name": "string.quoted.raw.fig",
"begin": "r\"",
"end": "\"",
"patterns": [{ "match": ".", "name": "string.content.fig" }]
}
]
},
"numbers": {
"patterns": [
{
"name": "constant.numeric.float.fig",
"match": "\\d*\\.\\d+([eE][+-]?\\d+)?"
},
{
"name": "constant.numeric.integer.fig",
"match": "\\d+([eE][+-]?\\d+)?"
}
]
},
"keywords": {
"patterns": [
{
"name": "keyword.control.fig",
"match": "\\b(and|or|not|import|func|var|const|final|while|for|if|else|new|struct|interface|impl|public|return|break|continue|try|catch|throw|is|as)\\b"
},
{ "name": "constant.language.fig", "match": "\\b(true|false|null)\\b" }
]
},
"operators": {
"patterns": [
{
"name": "keyword.operator.arithmetic.fig",
"match": "(\\+|\\-|\\*|/|%|\\*\\*)"
},
{
"name": "keyword.operator.assignment.fig",
"match": "(=|\\+=|\\-=|\\*=|/=|%=|\\^=|:=)"
},
{
"name": "keyword.operator.logical.fig",
"match": "(&&|\\|\\||\\b(and|or|not)\\b)"
},
{
"name": "keyword.operator.comparison.fig",
"match": "(==|!=|<=|>=|<|>)"
},
{
"name": "punctuation.separator.fig",
"match": "[\\(\\)\\[\\]\\{\\},;:.]"
},
{
"name": "keyword.operator.other.fig",
"match": "(\\+\\+|--|->|=>|<<|>>|\\^|&|\\||~)"
}
]
},
"functions": {
"patterns": [
{
"name": "entity.name.function.fig",
"begin": "\\bfunc\\s+([a-zA-Z_][a-zA-Z0-9_]*)",
"beginCaptures": {
"1": { "name": "entity.name.function.fig" }
},
"end": "(?=;)"
}
]
},
"identifiers": {
"patterns": [
{
"name": "variable.other.fig",
"match": "(?!\\bfunc\\b)[a-zA-Z_][a-zA-Z0-9_]*"
}
]
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "Node16",
"target": "ES2022",
"outDir": "out",
"lib": [
"ES2022"
],
"sourceMap": true,
"rootDir": "src",
"strict": true, /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
}
}
+44
View File
@@ -0,0 +1,44 @@
# Welcome to your VS Code Extension
## What's in the folder
* This folder contains all of the files necessary for your extension.
* `package.json` - this is the manifest file in which you declare your extension and command.
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesnt yet need to load the plugin.
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
## Get up and running straight away
* Press `F5` to open a new window with your extension loaded.
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
* Find output from your extension in the debug console.
## Make changes
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
## Explore the API
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
## Run tests
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered.
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
* See the output of the test result in the Test Results view.
* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder.
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
* You can create folders inside the `test` folder to structure your tests any way you want.
## Go further
* [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns.
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
* Integrate to the [report issue](https://code.visualstudio.com/api/get-started/wrapping-up#issue-reporting) flow to get issue and feature requests reported by users.
+52 -14
View File
@@ -1,18 +1,56 @@
MIT License
MIT License (Fig)
Copyright (c) 2026 PuqiAR
Copyright (c) 2026 PuqiAR <im@puqiar.top>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
1. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
2. This project includes code from the following projects with their respective licenses:
- magic_enum (MIT License)
Copyright (c) 2019 - 2024 Daniil Goncharov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- json (MIT LICENSE) (for LSP Server JSON-RPC)
Copyright (c) 2013-2026 Niels Lohmann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" fill="#F28C28"/>
<path d="M135 100 H395 V195 H230 V265 H370 V360 H230 V430 H135 V100 Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 244 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M135 100 H395 V195 H230 V265 H370 V360 H230 V430 H135 V100 Z" fill="#F28C28"/>
</svg>

After

Width:  |  Height:  |  Size: 196 B

+30
View File
@@ -1,2 +1,32 @@
# Fig
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./Logo/LogoDark.svg">
<img src="./Logo/Logo.svg" alt="Fig Logo" width="200">
</picture>
> **🔔 Main Repository: https://git.fig-lang.cn/PuqiAR/Fig**
> *GitHub is only a mirror. Please submit issues and PRs to the main repository.*
[English](./README.md) | [中文](./README_zh-CN.md)
![License](https://img.shields.io/badge/license-MIT-blue)
![Status](https://img.shields.io/badge/status-0.6.0--alpha-yellow)
![Rust](https://img.shields.io/badge/Rust-100%25-orange)
![cargo](https://img.shields.io/badge/cargo-build-green)
![Platform](https://img.shields.io/badge/Windows%20·%20Linux%20·%20macOS-lightgrey)
**Fig** is a programming language that blends dynamic typing with optional static type annotations, built on reference-based value semantics.
> **v0.6.0 — Rust rewrite in progress.**
## Progress
```
Lexer ████████████████████ 100% (43 tests, all pass)
Parser ░░░░░░░░░░░░░░░░░░░░ 0%
Analyzer ░░░░░░░░░░░░░░░░░░░░ 0%
Compiler ░░░░░░░░░░░░░░░░░░░░ 0%
VM ░░░░░░░░░░░░░░░░░░░░ 0%
LSP ░░░░░░░░░░░░░░░░░░░░ 0%
```
+32
View File
@@ -0,0 +1,32 @@
# Fig
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./Logo/LogoDark.svg">
<img src="./Logo/Logo.svg" alt="Fig Logo" width="200">
</picture>
> **🔔 主仓库:https://git.fig-lang.cn/PuqiAR/Fig**
> *GitHub 仅为镜像,Issue 与 PR 请提交至主仓库*
[English](./README.md) | [中文](./README_zh-CN.md)
![License](https://img.shields.io/badge/license-MIT-blue)
![Status](https://img.shields.io/badge/status-0.6.0--alpha-yellow)
![Rust](https://img.shields.io/badge/Rust-100%25-orange)
![cargo](https://img.shields.io/badge/cargo-构建-green)
![Platform](https://img.shields.io/badge/Windows%20·%20Linux%20·%20macOS-lightgrey)
**Fig** 是一门动态类型与静态类型注解混合的编程语言,采用基于引用的值语义。
> **v0.6.0 — Rust 重写进行中。**
## 进度
```
Lexer ████████████████████ 100% (43 测试, 全部通过)
Parser ░░░░░░░░░░░░░░░░░░░░ 0%
Analyzer ░░░░░░░░░░░░░░░░░░░░ 0%
Compiler ░░░░░░░░░░░░░░░░░░░░ 0%
VM ░░░░░░░░░░░░░░░░░░░░ 0%
LSP ░░░░░░░░░░░░░░░░░░░░ 0%
```
+63
View File
@@ -0,0 +1,63 @@
use std::process::Command;
fn main() {
let hash = Command::new("git")
.args(["rev-parse", "--short=7", "HEAD"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.unwrap_or_default()
.trim()
.to_string();
println!("cargo:rustc-env=GIT_HASH={}", hash);
println!("cargo:rustc-env=BUILD_TIME={}", chrono_build_time());
// 编译器 IDmacOS 上用 clang/LLVMLinux 上用 GCCWindows 上用 MSVC
#[cfg(target_os = "macos")]
{ println!("cargo:rustc-env=FIG_COMPILER_ID=LLVM"); }
#[cfg(target_os = "linux")]
{ println!("cargo:rustc-env=FIG_COMPILER_ID=GCC"); }
#[cfg(target_os = "windows")]
{ println!("cargo:rustc-env=FIG_COMPILER_ID=MSVC"); }
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{ println!("cargo:rustc-env=FIG_COMPILER_ID=unknown"); }
}
// chrono 还没引入,先用简单方式
fn chrono_build_time() -> String {
// RFC 3339 without nanoseconds: "2026-07-23T13:14:00+08:00"
// 但纯 std 拿不到时区,用环境变量 SOURCE_DATE_EPOCH 或 UTC
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let secs = now % 86400;
let days = now / 86400;
// 简单计算 UTC 时间,避免引入 chrono
let hour = (secs / 3600) % 24;
let min = (secs / 60) % 60;
let sec = secs % 60;
// 粗略日期计算(从 1970-01-01 开始)
let (y, mo, d) = civil_from_days(days as i64);
format!("{y:04}-{mo:02}-{d:02} {hour:02}:{min:02}:{sec:02} UTC")
}
// 从 Unix epoch 天数反算年月日
fn civil_from_days(days: i64) -> (i64, u32, u32) {
let z = days + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u32;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

+24
View File
@@ -0,0 +1,24 @@
# Fig 语言简介
## 概述
Fig 是一门动态类型、解释执行的编程语言,专注于简洁语法和实用的语言特性。它采用树遍历解释器架构,支持多种编程范式。
## 实际观察到的特性
1. **解释执行**:基于 AST 的树遍历解释器,无编译步骤
2. **动态类型系统**:运行时类型检查,支持类型注解但不强制
3. **混合范式**:支持函数式、面向对象和命令式风格
4. **模块系统**:支持代码组织和复用
5. **内置类型**:整数、浮点数、字符串、列表、映射等
6. **垃圾回收**:基于引用计数的自动内存管理
## 语言设计特点
- **渐进类型**:支持类型注解但不强制,兼顾灵活性和可读性
- **一等公民函数**:函数可以作为参数传递和返回
- **闭包支持**:完整的词法作用域和闭包
- **错误处理**:异常机制和 try-catch 结构
- **可变与不可变**:const/var 区分,平衡安全与灵活
## 目标应用场景
- 脚本编写和自动化任务
- 教育用途和学习编程
- 配置语言和DSL
+82
View File
@@ -0,0 +1,82 @@
# 快速开始
## 运行 Fig 程序
Fig 语言通过解释器直接执行源代码文件。基本执行命令如下:
`./Fig 你的脚本.fig`
### 查看帮助和版本
显示帮助信息:
`./Fig -h``./Fig --help`
显示版本信息:
`./Fig -v``./Fig --version`
### 示例
创建一个名为 `hello.fig` 的文件,内容为:
```go
import std.io;
io.println("Hello, Fig!");
```
在终端中运行:
`./Fig hello.fig`
你会看到输出:`Hello, Fig!`
## 程序示例
### 简单表达式程序
Fig 程序可以只包含表达式:
```go
1 + 2 * 3
```
运行此程序会输出计算结果 `7`
### 带变量的程序
```go
var x = 10;
var y = x * 2;
y + 5
```
运行输出 `25`
## 错误处理
当源代码有语法或类型错误时,解释器会显示详细的错误信息。例如:
```rust
An error occurred! Fig 0.4.2-alpha (2026-01-23 01:30:46)[llvm-mingw 64 bit on `Windows`]
TypeError: Variable `x` expects init-value type `Int`, but got 'Double'
at 1:14 in file 'your_file.fig'
var x: Int = 3.14;
^
```
错误信息包括:
- 错误类型和描述
- 发生错误的文件和位置
- 相关的堆栈跟踪信息
## 运行流程
1. **编写代码**:使用任何文本编辑器创建 `.fig` 文件
2. **保存文件**:确保文件扩展名为 `.fig`
3. **执行程序**:在终端中运行 `./Fig 文件名.fig`
4. **查看结果**:程序输出显示在终端中
5. **调试错误**:根据错误信息修改代码
## 注意事项
- Fig 源文件必须使用 UTF-8 编码
- 语句通常以分号结束,但程序最后一条表达式可以省略分号
- 文件路径可以包含空格,但建议使用引号包裹:`./Fig "my script.fig"`
+213
View File
@@ -0,0 +1,213 @@
# 基础语法
## 注释
Fig 支持两种注释格式:
单行注释:
```go
// 这是一个单行注释
var x = 10; // 注释可以在语句后面
```
多行注释:
```go
/* 这是一个
多行注释 */
/* 注释内可以有 // 嵌套的单行注释 */
```
注释不能嵌套多个多行注释:`/* /* 嵌套 */ */` 会导致错误。
## 变量声明
### 可变变量
```go
var name = "Fig";
var count = 0;
count = count + 1; // 可以重新赋值
```
### 常量
```go
const PI = 3.14159;
const MAX_SIZE = 100;
// PI = 3.14; // 错误:常量不能重新赋值
```
常量必须在声明时初始化。
### 类型注解(可选)
```go
var name: String = "Fig";
var age: Int = 14;
const VERSION: Double = 0.4;
```
类型注解提供额外的类型信息,但解释器会在运行时进行类型检查。
## 标识符命名
### 规则
- 可以包含字母、数字和下划线
- 不能以数字开头
- 区分大小写
- 支持 Unicode 字符
### 有效示例
```go
var count = 0;
var user_name = "Alice";
var 计数器 = 0; // 中文标识符
var π = 3.14159; // Unicode 符号
var _private = true; // 以下划线开头
```
### 无效示例
```
var 123abc = 0; // 不能以数字开头
var my-var = 0; // 不能包含连字符
```
## 基本字面量
### 数字
```go
// 整数
var a = 42; // 十进制
var b = -100; // 负数
var c = 0; // 零
// 浮点数
var d = 3.14;
var e = 1.0;
var f = -0.5;
var g = 1.23e-10; // 科学计数法
```
### 字符串
```go
var s1 = "Hello";
var s2 = "World";
var s3 = "包含\"引号\"的字符串"; // 转义引号
var s4 = "第一行\n第二行"; // 转义换行符
```
多行字符串直接跨行书写:
```rust
var message = "这是一个
多行字符串
可以包含多行内容";
```
### 布尔值
```go
var yes = true;
var no = false;
```
### 空值
```dart
var nothing = null;
```
## 分号使用
所有语句都必须以分号结束:
```go
var x = 10; // 正确
var y = 20 // 错误:缺少分号
func add(a, b) {
return a + b; // return 语句需要分号
} // 函数体右花括号后不需要分号
```
表达式作为独立语句时也需要分号:
```go
1 + 2 * 3; // 表达式语句需要分号
io.println("test"); // 函数调用语句需要分号
```
## 表达式与语句
### 表达式
表达式会产生一个值(包括 `null`):
```go
1 + 2 // 值为 3
x * y // 值为乘积
funcCall(arg) // 值为函数返回值
```
### 语句
语句执行操作但不产生值:
```go
var x = 10; // 声明语句
if condition {} // 条件语句
return value; // 返回语句
```
表达式可以作为语句使用(表达式语句):
```go
1 + 2; // 计算但丢弃结果
io.println("hi"); // 函数调用作为语句
```
## 关键字
Fig 语言的关键字包括:
| 类别 | 关键字 |
| -------- | ----------------------------------------------------------- |
| 声明 | `func`, `var`, `const`, `struct`, `interface`, `import` |
| 控制流 | `if`, `else`, `while`, `for`, `return`, `break`, `continue` |
| 错误处理 | `try`, `catch`, `throw`, `finally` |
| 逻辑运算 | `and`, `or`, `not` |
| 类型相关 | `is`, `as`, `impl`, `new`, `public` |
这些关键字不能用作标识符名称。
## 操作符
### 算术运算符
`+`, `-`, `*`, `/`, `%`, `**`(幂运算)
### 比较运算符
`==`, `!=`, `<`, `>`, `<=`, `>=`
### 逻辑运算符
`and`, `or`, `not`, `&&`, `||`, `!`
### 位运算符
`&`, `|`, `^`, `~`, `<<`, `>>`
### 赋值运算符
`=`, `:=`, `+=`, `-=`, `*=`, `/=`, `%=`, `^=`
### 其他运算符
`.`(成员访问), `?`(三元运算), `...`(可变参数), `->`(箭头), `=>`(双箭头)
## 空白字符
Fig 对空白字符没有特殊要求:
- 空格和制表符可以互换使用
- 缩进不影响程序语义
- 操作符周围的空格是可选的(但建议添加以提高可读性)
## 代码示例
```go
// 完整的程序示例
import std.io;
func calculate(a: Int, b: Int) -> Int {
var result = a * b;
return result + 10;
}
const x = 5;
const y = 3;
var answer = calculate(x, y);
io.println("结果是:" + answer.toString());
```
+156
View File
@@ -0,0 +1,156 @@
# 数据类型
## 类型系统
Fig 语言是动态类型语言,运行时进行类型检查。
## 基本类型
### Null 类型
表示空值,只有一个值 `null`
```dart
var a = null;
```
### Int 类型
64位有符号整数。
```go
var x = 42;
var y = -100;
```
### Double 类型
双精度浮点数。
```go
var pi = 3.14;
var speed = 2.998e8;
```
### String 类型
字符串。
```go
var s1 = "Hello";
```
### Bool 类型
布尔值,`true``false`
```go
var yes = true;
var no = false;
```
## 复合类型
### List 类型
有序集合。
```go
var list = [1, 2, 3];
var mixed = [1, "text", true];
```
可以通过 **length()** 方法获取长度,返回Int
通过 **get()** 方法获取指定下标,不存在的下标返回null
### Map 类型
键值对集合。
```go
var map = {"key": "value", "num": 42};
```
通过 **get()** 方法获取指定下标,不存在的下标返回 `null`
### Function 类型
函数。
```go
var f = func(x, y) { return x + y; };
```
## 自定义类型
### 结构体定义
```rust
struct Person {
name: String;
age: Int;
}
```
默认字段为私有,如需声明为外部可见的字段,使用 public关键字
```go
struct Person {
public name: String;
public age: Int;
public func greeting()
{
io.println("hello!");
}
}
```
### 创建实例
使用 `new` 关键字:
```go
var p = new Person {name: "Alice", age: 20};
```
支持三种初始化方式:
1. 命名参数:`new Person {name: "Alice", age: 20}`
2. 位置参数:`new Person {"Alice", 20}`
3. 简写方式:`new Person {name, age}`(使用同名变量)
## 类型操作
### 类型检查
使用 `is` 运算符:
```go
var x = "hello";
io.println(x is String); // true
var p = new Person {name: "Alice", age: 20};
io.println(p is Person); // true
```
### 获取类型
使用 `value.type()` 函数(获取内部类型)
```go
import std.value;
var num = 42;
var str = "hello";
var lst = [1, 2, 3];
io.println(value.type(num)); // 获取 num 的类型
io.println(value.type(str)); // 获取 str 的类型
io.println(value.type(lst)); // 获取 lst 的类型
```
## 类型转换
### 转换函数
通过 `std.value` 模块:
```go
import std.value;
var str = value.string_from(42); // "42"
var num = value.int_parse("123"); // 123
var dbl = value.double_parse("3.14"); // 3.14
```
## 错误处理
### try-catch 语法
```rust
try
{
// 可能抛出异常的代码
}
catch(e: ErrorType)
{
// 处理特定类型的错误
}
catch(e: AnotherError)
{
// 处理另一种错误
}
```
+289
View File
@@ -0,0 +1,289 @@
# 函数
## 函数定义
### 基本语法
```go
func 函数名(参数列表) -> 返回类型 {
函数体
}
```
### 示例
```go
// 简单函数
func greet() -> Null {
io.println("Hello!");
}
// 带参数的函数
func add(a: Int, b: Int) -> Int {
return a + b;
}
// 省略返回类型(默认为 Any
func say(message: String) {
io.println(message);
// 没有 return,返回 null
}
```
## 参数
### 必需参数
```go
func power(base: Double, exponent: Double) -> Double {
return base ** exponent;
}
```
### 默认参数
参数可以指定默认值:
```go
func createPerson(name: String, age: Int = 18) -> Null {
io.println(name + " is " + age + " years old");
}
// 调用
createPerson("Alice"); // 使用默认 age=18
createPerson("Bob", 25); // 指定 age=25
```
### 可变参数
使用 `...` 表示可变参数,接收一个 List
```go
func sum(numbers...) -> Int {
var total = 0;
var i = 0;
for i = 0; i < numbers.length(); i = i + 1 {
total = total + numbers.get(i);
}
return total;
}
// 调用
sum(1, 2, 3); // 返回 6
sum(1, 2, 3, 4, 5); // 返回 15
```
可变参数不支持类型限制,获取到的总是 List 类型。
## 返回值
### 显式返回
使用 `return` 语句返回值:
```go
func max(a: Int, b: Int) -> Int {
if a > b {
return a;
} else {
return b;
}
}
```
### 无返回值
函数如果没有 return 语句,返回 null
```go
func log(message: String) -> Null {
io.println("[LOG] " + message);
// 函数结束,返回 null
}
func process(data: List) {
if data.length() == 0 {
return; // 提前返回 null
}
// 处理数据...
// 函数结束,返回 null
}
```
## 函数作为值
### 函数赋值
函数是一等公民,可以赋值给变量:
```go
var addFunc = func(a: Int, b: Int) -> Int {
return a + b;
};
// 调用
var result = addFunc(3, 4); // result = 7
```
### 函数作为参数
```go
func applyOperation(x: Int, y: Int, op: Function) -> Int {
return op(x, y);
}
func multiply(a: Int, b: Int) -> Int {
return a * b;
}
// 调用
var product = applyOperation(3, 4, multiply); // product = 12
```
### 函数作为返回值
```go
func makeMultiplier(factor: Int) -> Function {
return func(x: Int) -> Int {
return x * factor;
};
}
// 调用
var double = makeMultiplier(2);
var triple = makeMultiplier(3);
io.println(double(5)); // 输出 10
io.println(triple(5)); // 输出 15
```
## 匿名函数
### Lambda 表达式
```go
// 完整形式
var square = func(x: Int) {
return x * x;
};
// 简写形式(单表达式)
var squareShort = func(x: Int) => x * x;
// 调用
io.println(square(5)); // 输出 25
io.println(squareShort(5)); // 输出 25
```
### 立即调用函数表达式
```go
var result = func(x: Int, y: Int){
return x + y;
}(3, 4); // result = 7
```
## 闭包
### 捕获外部变量
函数可以捕获其定义作用域中的变量:
```go
func makeCounter() -> Function {
var count = 0;
return func(){
count = count + 1;
return count;
};
}
// 使用
var counter = makeCounter();
io.println(counter()); // 输出 1
io.println(counter()); // 输出 2
io.println(counter()); // 输出 3
```
每个闭包有自己独立的捕获变量:
```go
var c1 = makeCounter();
var c2 = makeCounter();
io.println(c1()); // 输出 1
io.println(c1()); // 输出 2
io.println(c2()); // 输出 1(独立的计数)
```
## 递归函数
函数可以调用自身:
```go
func factorial(n: Int) -> Int {
if n <= 1 {
return 1;
}
return n * factorial(n - 1);
}
// 调用
io.println(factorial(5)); // 输出 120
```
### 嵌套函数定义
```go
func outer(x: Int) -> Int {
func inner(y: Int) -> Int {
return y * 2;
}
return inner(x) + 1;
}
// 调用
io.println(outer(10)); // 输出 21
```
## 函数调用
### 普通调用
```go
func calculate(a: Int, b: Int, c: Int) -> Int {
return a + b * c;
}
// 位置参数调用
var result = calculate(1, 2, 3); // 1 + 2*3 = 7
```
### 方法调用语法
对象的方法调用:
```go
var list = [1, 2, 3];
var length = list.length(); // 方法调用
```
## 函数示例
### 实用函数组合
```go
import std.io;
// 高阶函数示例
func compose(f: Function, g: Function) -> Function {
return func(x: Any) -> Any {
return f(g(x));
};
}
// 使用
func addOne(x: Int) -> Int {
return x + 1;
}
func double(x: Int) -> Int {
return x * 2;
}
var addThenDouble = compose(double, addOne);
io.println(addThenDouble(5)); // (5+1)*2 = 12
```
### 回调函数模式
```go
func processData(data: List, callback: Function) -> Null {
var i = 0;
for i = 0; i < data.length(); i = i + 1 {
var result = callback(data.get(i));
io.println("处理结果: " + result);
}
}
// 使用
var numbers = [1, 2, 3, 4, 5];
processData(numbers, func(x){
return x * x;
});
```
+126
View File
@@ -0,0 +1,126 @@
# 面对对象
> Fig中只有结构体(`struct`) 遵循go圣经 组合由于继承 (struct组合尚未推出)
## 结构体定义
完整语法:
```cpp
struct Point
{
public x: Int; // 公开字段
public y: Int; // 公开字段
}
struct Person
{
name: String; // 私有字段,无法被外部访问
age: Int; // 私有字段,无法被外部访问
sex: String = "Airplane"; // 私有字段,无法被外部访问
public func getName() -> String // 公开类函数
{
return name;
}
}
```
## 结构体初始化
语法:
```go
// 位置参数构造
var person := new Person{
"Fig", // name
1, // age
"Language" // sex
};
```
```go
// 命名参数构造模式,可以无序
var person := new Person{
name: "Fig",
age: 1,
sex: "Language"
};
```
```go
// 语法糖:同名变量构造
const name := "Fig";
const age := 1;
const sex := "Language";
var person := new Person{sex, name, age}; // 可以无序,自动匹配
```
请注意,同名变量构造(shorthand)模式请规范使用:
&nbsp;
示例:定义
```cpp
struct Point
{
public x;
public y;
}
```
使用:
```go
var a := Point{1,2};
io.println(a.x, a.y); // 1 2
var x := 7;
var y := 6;
var b := Point{y, x};
io.println(b.x, b.y); // 7 6
// ??
```
使用该模式最好有序构造,如果你清楚你在做什么,完全可以利用它
## 结构体运算符重载
**目前不支持**
## 接口与实现
`interface``implement`
### 定义接口
```go
interface Printable
{
toString() -> String;
getName() -> String
{
return "Printable";
}
}
```
使用 `interface` + 名字 {内容}定义结构体
方法签名为 `method() -> type`,其中必须提供返回类型,这是约定。
提供默认方法将在子类实现为实现情况下自动替补
### 实现
```rust
struct Person
{
name: String;
age: Int;
}
impl Printable for Person
{
toString()
{
return name + ":" + age;
}
getName()
{
return "Person";
}
}
```
实现时不需要也不允许提供返回类型,必须与`interface`约定一致
内部通过动态派发 vtable实现
+48
View File
@@ -0,0 +1,48 @@
/*
src/core/build_info.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
use crate::core::colors;
pub struct BuildInfo
{
pub version: &'static str,
pub git_hash: &'static str,
pub build_time: &'static str,
pub platform: String,
}
pub fn get() -> BuildInfo
{
BuildInfo
{
version: env!("CARGO_PKG_VERSION"),
git_hash: env!("GIT_HASH"),
build_time: env!("BUILD_TIME"),
platform: format!(
"{} [{} | {}]",
std::env::consts::OS,
env!("FIG_COMPILER_ID"),
std::env::consts::ARCH
),
}
}
pub fn print_header(sub_system: &str)
{
let info = get();
print!(
"{}{} v{} {}(Build {} {} {}){}\n\n",
colors::BOLD,
sub_system,
info.version,
colors::DIM,
info.git_hash,
info.build_time,
info.platform,
colors::RESET,
);
}
+32
View File
@@ -0,0 +1,32 @@
/*
src/core/colors.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
pub const RESET: &str = "\x1b[0m";
pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m";
pub const RED: &str = "\x1b[31m";
pub const GREEN: &str = "\x1b[32m";
pub const YELLOW: &str = "\x1b[33m";
pub const CYAN: &str = "\x1b[36m";
pub const BRIGHT_RED: &str = "\x1b[91m";
pub const BRIGHT_CYAN: &str = "\x1b[96m";
pub const GRAY: &str = "\x1b[90m";
pub const LIGHT_GRAY: &str = "\x1b[37m";
pub const ORANGE: &str = "\x1b[38;2;217;119;6m";
pub const PURPLE: &str = "\x1b[38;2;168;85;247m"; // 亮紫色 error 主色
pub const CRITICAL_RED: &str = "\x1b[38;2;239;68;68m"; // critical
pub const ACCENT_BLUE: &str = "\x1b[38;2;59;130;246m";
// 源码行号
pub const LINE_NO: &str = "\x1b[38;2;59;130;246m"; // 蓝,和 error 区分
// 源码文本
pub const LIGHT_BLUE: &str = "\x1b[38;2;96;165;250m";
pub const SOURCE_TEXT: &str = "\x1b[38;2;180;180;180m"; // 浅灰,比 DIM 亮
+18
View File
@@ -0,0 +1,18 @@
/*
src/core/mod.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
pub mod source;
pub mod colors;
pub mod build_info;
#[derive(Debug, Clone, Copy)]
#[allow(non_camel_case_types)]
pub enum Lang
{
en_US,
zh_CN,
}
+124
View File
@@ -0,0 +1,124 @@
/*
src/core/source.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
use std::path::PathBuf;
#[derive(Debug, Clone, Copy)]
pub struct SourcePosition
{
pub file_id: usize,
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct SourceRange
{
pub file_id: usize,
pub start_line: usize,
pub start_column: usize,
pub end_line: usize,
pub end_column: usize,
}
// SourceFile / SourceManager
pub struct SourceFile
{
pub path: PathBuf,
pub source: String,
line_offsets: Vec<usize>,
}
impl SourceFile
{
pub fn from_string(name: &str, source: String) -> Self
{
let line_offsets = Self::build_line_offsets(&source);
Self { path: PathBuf::from(name), source, line_offsets }
}
pub fn from_path(path: PathBuf) -> Result<Self, std::io::Error>
{
let source = std::fs::read_to_string(&path)?;
let line_offsets = Self::build_line_offsets(&source);
Ok(Self { path, source, line_offsets })
}
pub fn read(path: PathBuf) -> Result<Self, std::io::Error>
{
let source = std::fs::read_to_string(&path)?;
let line_offsets = Self::build_line_offsets(&source);
Ok(Self { path, source, line_offsets })
}
fn build_line_offsets(source: &str) -> Vec<usize>
{
std::iter::once(0)
.chain(
source
.bytes()
.enumerate()
.filter(|(_, b)| *b == b'\n')
.map(|(i, _)| i + 1),
)
.collect()
}
pub fn line_offsets(&self) -> &[usize] { &self.line_offsets }
pub fn offset_to_position(&self, offset: usize) -> (usize, usize)
{
let line = match self.line_offsets.binary_search(&offset) {
Ok(line) => line,
Err(line) => line.saturating_sub(1),
};
let col = offset - self.line_offsets[line] + 1;
(line + 1, col)
}
pub fn get_line(&self, offset: usize) -> &str
{
let (line, _) = self.offset_to_position(offset);
let start = self.line_offsets[line - 1];
let end = self.source[start..]
.find('\n')
.map(|i| start + i)
.unwrap_or(self.source.len());
&self.source[start..end]
}
}
pub struct SourceManager
{
files: Vec<SourceFile>,
}
impl SourceManager
{
pub fn new() -> Self
{
Self { files: Vec::new() }
}
pub fn add_file(&mut self, file: SourceFile) -> usize
{
let id = self.files.len();
self.files.push(file);
id
}
pub fn get(&self, id: usize) -> Option<&SourceFile>
{
self.files.get(id)
}
pub fn source(&self, id: usize) -> Option<&str>
{
self.files.get(id).map(|f| f.source.as_str())
}
}
@@ -0,0 +1,63 @@
/*
src/error/definitions/invalid_escape_sequence.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
use crate::error::diagnostic::{Diagnostic, ErrorType, Thrower};
#[derive(Debug)]
pub struct InvalidEscapeSequenceError {
file_id: usize,
start_line: usize,
start_column: usize,
escape_sequence: String,
thrower: Option<Thrower>,
}
impl InvalidEscapeSequenceError {
pub fn new(file_id: usize, start_line: usize, start_column: usize, escape_sequence: String) -> Self {
InvalidEscapeSequenceError {
file_id,
start_line,
start_column,
escape_sequence,
thrower: None,
}
}
pub fn with_thrower(mut self, thrower: Thrower) -> Self {
self.thrower = Some(thrower);
self
}
}
impl Diagnostic for InvalidEscapeSequenceError {
fn error_type(&self) -> ErrorType {
ErrorType::InvalidEscapeSequence
}
fn message(&self, lang: crate::core::Lang) -> String {
match lang {
crate::core::Lang::en_US => format!(
"Invalid escape sequence: {} at line {}, column {}",
self.escape_sequence, self.start_line, self.start_column
),
crate::core::Lang::zh_CN => format!(
"无效的转义序列: {} 在第 {} 行,第 {}",
self.escape_sequence, self.start_line, self.start_column
),
}
}
fn span(&self) -> crate::core::source::SourceRange {
crate::core::source::SourceRange {
file_id: self.file_id,
start_line: self.start_line,
start_column: self.start_column,
end_line: self.start_line,
end_column: self.start_column + self.escape_sequence.len(),
}
}
}
@@ -0,0 +1,72 @@
/*
src/error/definitions/invalid_number_literal.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
use crate::core;
use crate::core::source::SourceRange;
use crate::error::diagnostic::{Diagnostic, ErrorType, Thrower};
#[derive(Debug)]
pub struct InvalidNumberLiteralError {
file_id: usize,
start_line: usize,
start_column: usize,
end_column: usize,
thrower: Option<Thrower>,
}
impl InvalidNumberLiteralError {
pub fn new(file_id: usize, start_line: usize, start_column: usize, end_column: usize) -> Self {
InvalidNumberLiteralError {
file_id,
start_line,
start_column,
end_column,
thrower: None,
}
}
pub fn with_thrower(mut self, thrower: Thrower) -> Self {
self.thrower = Some(thrower);
self
}
}
impl Diagnostic for InvalidNumberLiteralError {
fn error_type(&self) -> ErrorType {
ErrorType::InvalidNumberLiteral
}
fn message(&self, lang: core::Lang) -> String {
match lang {
core::Lang::en_US => {
format!(
"Invalid number literal at line {}, column {}",
self.start_line, self.start_column,
)
}
core::Lang::zh_CN => {
format!(
"无效的数字字面量,位于第 {} 行,第 {}",
self.start_line, self.start_column,
)
}
}
}
fn span(&self) -> SourceRange {
SourceRange {
file_id: self.file_id,
start_line: self.start_line,
start_column: self.start_column,
end_line: self.start_line,
end_column: self.end_column,
}
}
fn thrower(&self) -> Option<Thrower> {
self.thrower
}
}
+12
View File
@@ -0,0 +1,12 @@
/*
src/error/definitions/mod.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
pub mod unterminated_string_literal;
pub mod unexpected_character;
pub mod invalid_number_literal;
pub mod invalid_escape_sequence;
pub mod unterminated_comment;
@@ -0,0 +1,90 @@
/*
src/error/definitions/unexpected_character.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
use crate::core;
use crate::core::source::SourceRange;
use crate::error::diagnostic::{Diagnostic, ErrorType, Thrower};
#[derive(Debug)]
pub struct UnexpectedCharacterError
{
file_id: usize,
character: char,
line: usize,
column: usize,
thrower: Option<Thrower>,
}
impl UnexpectedCharacterError
{
pub fn new(file_id: usize, character: char, line: usize, column: usize) -> Self
{
UnexpectedCharacterError
{
file_id,
character,
line,
column,
thrower: None,
}
}
pub fn with_thrower(mut self, t: Thrower) -> Self
{
self.thrower = Some(t);
self
}
}
impl Diagnostic for UnexpectedCharacterError
{
fn error_type(&self) -> ErrorType
{
ErrorType::UnexpectedCharacter
}
fn message(&self, lang: crate::core::Lang) -> String
{
let ch = match (self.character, lang) {
(' ', crate::core::Lang::zh_CN) => "<空格>",
(' ', _) => "<space>",
('\n', crate::core::Lang::zh_CN) => "<换行>",
('\n', _) => "<newline>",
('\t', crate::core::Lang::zh_CN) => "<制表符>",
('\t', _) => "<tab>",
('\r', crate::core::Lang::zh_CN) => "<回车>",
('\r', _) => "<carriage return>",
(other, crate::core::Lang::zh_CN) =>
return format!("意外的字符 '{}',位于第 {} 行,第 {}", other, self.line, self.column),
(other, _) =>
return format!("Unexpected character '{}' at line {}, column {}", other, self.line, self.column),
};
match lang {
crate::core::Lang::zh_CN =>
format!("意外的字符 {},位于第 {} 行,第 {}", ch, self.line, self.column),
_ =>
format!("Unexpected character {} at line {}, column {}", ch, self.line, self.column),
}
}
fn span(&self) -> SourceRange
{
SourceRange
{
file_id: self.file_id,
start_line: self.line,
start_column: self.column,
end_line: self.line,
end_column: self.column + 1,
}
}
fn thrower(&self) -> Option<Thrower>
{
self.thrower
}
}
@@ -0,0 +1,83 @@
/*
src/error/definitions/unterminated_comment.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
use crate::error::diagnostic::{Diagnostic, ErrorType, Thrower};
use crate::core;
use crate::core::source::SourceRange;
#[derive(Debug)]
pub struct UnterminatedCommentError
{
file_id: usize,
start_line: usize,
start_column: usize,
end_line: usize,
end_column: usize,
thrower: Option<Thrower>,
}
impl UnterminatedCommentError
{
pub fn new(file_id: usize, start_line: usize, start_column: usize, end_line: usize, end_column: usize) -> Self
{
UnterminatedCommentError
{
file_id,
start_line,
start_column,
end_line,
end_column,
thrower: None,
}
}
pub fn with_thrower(mut self, t: Thrower) -> Self
{
self.thrower = Some(t);
self
}
}
impl Diagnostic for UnterminatedCommentError
{
fn error_type(&self) -> ErrorType
{
ErrorType::UnterminatedComment
}
fn message(&self, lang: core::Lang) -> String
{
match lang
{
core::Lang::en_US =>
{
format!("Unterminated block comment starting at line {}, column {}", self.start_line, self.start_column)
}
core::Lang::zh_CN =>
{
format!("未终止的多行注释,起始于第 {} 行,第 {}", self.start_line, self.start_column)
}
}
}
fn span(&self) -> SourceRange
{
SourceRange
{
file_id: self.file_id,
start_line: self.start_line,
start_column: self.start_column,
end_line: self.end_line,
end_column: self.end_column,
}
}
fn thrower(&self) -> Option<Thrower>
{
self.thrower
}
}
@@ -0,0 +1,83 @@
/*
src/error/definitions/unterminated_string_literal.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
use crate::error::diagnostic::{Diagnostic, ErrorType, Thrower};
use crate::core;
use crate::core::source::SourceRange;
#[derive(Debug)]
pub struct UnterminatedStringLiteralError
{
file_id: usize,
start_line: usize,
start_column: usize,
end_line: usize,
end_column: usize,
thrower: Option<Thrower>,
}
impl UnterminatedStringLiteralError
{
pub fn new(file_id: usize, start_line: usize, start_column: usize, end_line: usize, end_column: usize) -> Self
{
UnterminatedStringLiteralError
{
file_id,
start_line,
start_column,
end_line,
end_column,
thrower: None,
}
}
pub fn with_thrower(mut self, t: Thrower) -> Self
{
self.thrower = Some(t);
self
}
}
impl Diagnostic for UnterminatedStringLiteralError
{
fn error_type(&self) -> ErrorType
{
ErrorType::UnterminatedStringLiteral
}
fn message(&self, lang: core::Lang) -> String
{
match lang
{
core::Lang::en_US =>
{
format!("Unterminated string literal starting at line {}, column {}", self.start_line, self.start_column)
}
core::Lang::zh_CN =>
{
format!("字符串字面量未终止,起始于第 {} 行,第 {}", self.start_line, self.start_column)
}
}
}
fn span(&self) -> SourceRange
{
SourceRange
{
file_id: self.file_id,
start_line: self.start_line,
start_column: self.start_column,
end_line: self.end_line,
end_column: self.end_column,
}
}
fn thrower(&self) -> Option<Thrower>
{
self.thrower
}
}
+137
View File
@@ -0,0 +1,137 @@
/*
src/error/error.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
use crate::core;
use crate::core::source::SourceRange;
use crate::error::hint;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity
{
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorType
{
UnterminatedStringLiteral,
UnexpectedCharacter,
InvalidNumberLiteral,
InvalidEscapeSequence,
UnterminatedComment,
}
impl ErrorType
{
pub fn severity(&self) -> Severity
{
match self
{
ErrorType::UnterminatedStringLiteral => Severity::Error,
ErrorType::UnexpectedCharacter => Severity::Error,
ErrorType::InvalidNumberLiteral => Severity::Error,
ErrorType::InvalidEscapeSequence => Severity::Error,
ErrorType::UnterminatedComment => Severity::Error,
}
}
pub fn title(&self, lang: core::Lang) -> String
{
match self
{
ErrorType::UnterminatedStringLiteral =>
{
match lang
{
core::Lang::en_US => "Unterminated string literal".to_string(),
core::Lang::zh_CN => "字符串字面量未终止".to_string(),
}
}
ErrorType::UnexpectedCharacter =>
{
match lang
{
core::Lang::en_US => "Unexpected character".to_string(),
core::Lang::zh_CN => "意外的字符".to_string(),
}
}
ErrorType::InvalidNumberLiteral =>
{
match lang
{
core::Lang::en_US => "Invalid number literal".to_string(),
core::Lang::zh_CN => "无效的数字字面量".to_string(),
}
}
ErrorType::InvalidEscapeSequence =>
{
match lang
{
core::Lang::en_US => "Invalid escape sequence".to_string(),
core::Lang::zh_CN => "无效的转义序列".to_string(),
}
}
ErrorType::UnterminatedComment =>
{
match lang
{
core::Lang::en_US => "Unterminated comment".to_string(),
core::Lang::zh_CN => "未终止的注释".to_string(),
}
}
}
}
}
pub struct Related
{
pub span: SourceRange,
pub message: String,
}
/// 编译器源码位置,`thrower!()` 宏自动捕获。
#[derive(Debug, Clone, Copy)]
pub struct Thrower
{
pub file: &'static str,
pub line: u32,
}
#[macro_export]
macro_rules! thrower {
() => {
$crate::error::diagnostic::Thrower {
file: file!(),
line: line!(),
}
};
}
pub trait Diagnostic: std::fmt::Debug
{
fn error_type(&self) -> ErrorType;
fn message(&self, lang: core::Lang) -> String;
fn span(&self) -> SourceRange;
fn hints(&self) -> Vec<hint::Hint>
{
vec![]
}
fn related(&self, _lang: core::Lang) -> Vec<Related>
{
vec![]
}
fn thrower(&self) -> Option<Thrower>
{
None
}
}
+13
View File
@@ -0,0 +1,13 @@
/*
src/error/hint.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
pub enum Hint
{
Insertion { line: usize, column: usize, content: String },
Deletion { line: usize, column: usize, length: usize },
Replacement { line: usize, column: usize, length: usize, content: String },
}
+11
View File
@@ -0,0 +1,11 @@
/*
src/error/mod.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
pub mod diagnostic;
pub mod definitions;
pub mod hint;
pub mod report;
+536
View File
@@ -0,0 +1,536 @@
/*
src/error/report.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
use std::fmt::Write;
use crate::core;
use crate::core::colors as C;
use crate::core::source::SourceManager;
use crate::error::diagnostic::{Diagnostic, Related, Severity};
use crate::error::hint::Hint;
pub struct Reporter {
lang: core::Lang,
}
impl Reporter {
pub fn new(lang: core::Lang) -> Self {
Self { lang }
}
pub fn report(&self, diag: &dyn Diagnostic, manager: &SourceManager, out: &mut impl Write) {
let et = diag.error_type();
let severity = et.severity();
let span = diag.span();
let file = manager.get(span.file_id);
let (severity_color, severity_label, severity_icon) = match severity {
Severity::Warning => (C::ORANGE, self.txt_warning(), "\u{26A0}"),
Severity::Error => (C::PURPLE, self.txt_error(), "\u{2717}"),
Severity::Critical => (C::CRITICAL_RED, self.txt_critical(), "\u{2620}"),
};
writeln!(
out,
"{} {}{}{}[E{}]{} {}",
severity_icon,
C::BOLD,
severity_color,
severity_label,
et as usize,
C::RESET,
et.title(self.lang),
)
.unwrap();
if let Some(f) = file {
writeln!(
out,
" {}{}-->{} {}:{}:{}",
C::DIM,
C::GRAY,
C::RESET,
f.path.display(),
span.start_line,
span.start_column,
)
.unwrap();
}
self.print_source_context(diag, manager, out);
let hints = diag.hints();
for hint in &hints {
let label = self.fmt_hint(hint);
writeln!(
out,
" {}={}{} {}",
C::DIM,
C::RESET,
self.txt_suggestion(),
label
)
.unwrap();
}
if let Some(t) = diag.thrower() {
writeln!(
out,
" {}@ {}:{}{}",
C::DIM,
t.file, t.line,
C::RESET,
).unwrap();
}
let related = diag.related(self.lang);
if !related.is_empty() {
for rel in &related {
self.print_related(rel, manager, out);
}
}
writeln!(out).unwrap();
}
fn print_source_context(
&self,
diag: &dyn Diagnostic,
manager: &SourceManager,
out: &mut impl Write,
) {
let span = diag.span();
let severity = diag.error_type().severity();
let file = match manager.get(span.file_id) {
Some(f) => f,
None => return,
};
let line_color = match severity {
Severity::Warning => C::ORANGE,
Severity::Error => C::PURPLE,
Severity::Critical => C::CRITICAL_RED,
};
let total_lines = file.line_offsets().len();
let context_lines = 3;
let line_start = span.start_line.saturating_sub(context_lines).max(1);
let line_end = (span.end_line + 2).min(total_lines);
let max_line_width = format!("{}", line_end).len();
writeln!(out, " {}|{}", C::DIM, C::RESET).unwrap();
for line_no in line_start..=line_end {
let offset = file.line_offsets()[line_no - 1];
let line_text = file.get_line(offset);
let highlight = line_no >= span.start_line && line_no <= span.end_line;
if highlight {
writeln!(
out,
" {} {}{}{}{} |{} {}",
padding(max_line_width, line_no),
C::BOLD,
line_color,
line_no,
C::RESET,
C::SOURCE_TEXT,
line_text,
)
.unwrap();
} else {
writeln!(
out,
" {} {}{}{} |{} {}",
padding(max_line_width, line_no),
C::DIM,
line_no,
C::RESET,
C::SOURCE_TEXT,
line_text,
)
.unwrap();
}
if line_no == span.start_line {
let (caret_line, carets_end) =
self.build_caret_line(span, severity, file, line_no, max_line_width);
writeln!(out, "{caret_line}").unwrap();
let msg = diag.message(self.lang);
let msg_color = match severity {
Severity::Warning => C::ORANGE,
Severity::Error => C::PURPLE,
Severity::Critical => C::CRITICAL_RED,
};
let arrow_indent = carets_end.saturating_sub(3);
writeln!(
out,
"{} {}{}╰─{} {} {}",
" ".repeat(arrow_indent),
C::DIM,
C::GRAY,
C::RESET,
msg_color,
msg,
)
.unwrap();
}
if severity == Severity::Critical && line_no == span.end_line {
let text_before = take_chars(line_text, span.end_column.saturating_sub(1));
let dw = display_width(&text_before);
let span_dw = display_width(&take_chars_range(
line_text,
span.start_column.saturating_sub(1),
span.end_column,
));
let ww = char_width('\u{FE4D}').max(1);
let wave_line = "\u{FE4D}".repeat((span_dw + ww - 1) / ww);
let pad = " ".repeat(max_line_width + 5 + dw);
writeln!(
out,
"{} {}{}{}{}",
pad,
C::BRIGHT_RED,
C::BOLD,
wave_line,
C::RESET
)
.unwrap();
}
}
writeln!(out, " {}|{}", C::DIM, C::RESET).unwrap();
}
/// 返回 (caret行字符串, caret结束的显示列位置)
fn print_related(
&self,
rel: &Related,
manager: &SourceManager,
out: &mut impl Write,
)
{
let file = match manager.get(rel.span.file_id) {
Some(f) => f,
None => return,
};
writeln!(
out,
" {}={} \u{1F4A1} {}{}{}: {}",
C::DIM,
C::RESET,
C::LIGHT_BLUE, self.txt_note(), C::RESET,
rel.message,
).unwrap();
writeln!(
out,
" {}{}-->{} {}:{}:{}",
C::DIM,
C::GRAY,
C::RESET,
file.path.display(),
rel.span.start_line,
rel.span.start_column,
).unwrap();
let line_no = rel.span.start_line;
let offset = file.line_offsets()[line_no - 1];
let line_text = file.get_line(offset);
let max_line_width = format!("{}", line_no).len();
writeln!(out, " {}|{}", C::DIM, C::RESET).unwrap();
writeln!(
out,
" {} {}{}{} |{} {}",
padding(max_line_width, line_no),
C::DIM, line_no,
C::RESET,
C::SOURCE_TEXT,
line_text,
).unwrap();
let col_start = rel.span.start_column;
let col_end = rel.span.end_column.min(chars_count(line_text) + 1);
let len = col_end.saturating_sub(col_start);
if len > 0 {
let dw_before = display_width(&take_chars(line_text, col_start.saturating_sub(1)));
let indent = " ".repeat(max_line_width + 5 + dw_before);
let dw_error = display_width(&take_chars_range(line_text, col_start.saturating_sub(1), col_start.saturating_sub(1) + len));
let wave_w = char_width('\u{FE4D}').max(1);
let n_wave = (dw_error + wave_w - 1) / wave_w;
let wave = "\u{FE4D}".repeat(n_wave);
writeln!(out, "{}{}{}{}{}", indent, C::PURPLE, C::BOLD, wave, C::RESET).unwrap();
}
writeln!(out, " {}|{}", C::DIM, C::RESET).unwrap();
writeln!(out).unwrap();
}
fn build_caret_line(
&self,
span: core::source::SourceRange,
severity: Severity,
file: &core::source::SourceFile,
line_no: usize,
max_line_width: usize,
) -> (String, usize) {
let line_offset = file.line_offsets()[line_no - 1];
let line_text = file.get_line(line_offset);
let col_start = if line_no == span.start_line {
span.start_column
} else {
1
};
let col_end = if line_no == span.end_line {
span.end_column.min(chars_count(line_text) + 1)
} else {
chars_count(line_text) + 1
};
let len = col_end.saturating_sub(col_start);
if len == 0 {
return (String::new(), 0);
}
let text_before_caret = take_chars(line_text, col_start.saturating_sub(1));
let dw_before = display_width(&text_before_caret);
let indent_width = max_line_width + 5 + dw_before;
let indent = " ".repeat(indent_width);
let error_text = take_chars_range(
line_text,
col_start.saturating_sub(1),
col_start.saturating_sub(1) + len,
);
let dw_error = display_width(&error_text);
let wave_w = char_width('\u{FE4D}').max(1);
let n_wave = (dw_error + wave_w - 1) / wave_w;
let wave = "\u{FE4D}".repeat(n_wave);
let color = match severity {
Severity::Warning => C::ORANGE,
Severity::Error => C::PURPLE,
Severity::Critical => C::CRITICAL_RED,
};
let line = format!("{}{}{}{}{}", indent, color, C::BOLD, wave, C::RESET);
let end_col = indent_width + n_wave * wave_w;
(line, end_col)
}
pub fn report_all(
&self,
diags: &[&dyn Diagnostic],
manager: &SourceManager,
out: &mut impl Write,
) {
let mut warnings = 0u32;
let mut errors = 0u32;
for diag in diags {
match diag.error_type().severity() {
Severity::Warning => warnings += 1,
_ => errors += 1,
}
self.report(*diag, manager, out);
}
writeln!(
out,
"{}{}{}",
C::BOLD,
self.fmt_summary(errors, warnings),
C::RESET,
)
.unwrap();
}
// i18n
fn txt_warning(&self) -> &str {
match self.lang {
core::Lang::zh_CN => "警告",
core::Lang::en_US => "warning",
}
}
fn txt_error(&self) -> &str {
match self.lang {
core::Lang::zh_CN => "错误",
core::Lang::en_US => "error",
}
}
fn txt_critical(&self) -> &str {
match self.lang {
core::Lang::zh_CN => "严重错误",
core::Lang::en_US => "critical error",
}
}
fn txt_suggestion(&self) -> &str {
match self.lang {
core::Lang::zh_CN => "建议",
core::Lang::en_US => "suggestion",
}
}
fn txt_note(&self) -> &str {
match self.lang {
core::Lang::zh_CN => "提示",
core::Lang::en_US => "note",
}
}
fn fmt_hint(&self, hint: &Hint) -> String {
match hint {
Hint::Insertion { content, .. } => match self.lang {
core::Lang::zh_CN => format!(": 插入 `{}`", content),
core::Lang::en_US => format!(": insert `{}`", content),
},
Hint::Deletion { length, .. } => match self.lang {
core::Lang::zh_CN => format!(": 删除 {} 个字符", length),
core::Lang::en_US => format!(": delete {} character(s)", length),
},
Hint::Replacement { content, .. } => match self.lang {
core::Lang::zh_CN => format!(": 替换为 `{}`", content),
core::Lang::en_US => format!(": replace with `{}`", content),
},
}
}
fn fmt_summary(&self, errors: u32, warnings: u32) -> String {
match self.lang {
core::Lang::zh_CN => format!("{} 个错误,{} 个警告", errors, warnings),
core::Lang::en_US => format!("{} error(s), {} warning(s)", errors, warnings),
}
}
}
// 工具函数
fn padding(max: usize, n: usize) -> String {
let w = format!("{}", n).len();
" ".repeat(max.saturating_sub(w))
}
fn chars_count(s: &str) -> usize {
s.chars().count()
}
fn take_chars(s: &str, n: usize) -> String {
s.chars().take(n).collect()
}
fn take_chars_range(s: &str, start: usize, end: usize) -> String {
s.chars()
.skip(start)
.take(end.saturating_sub(start))
.collect()
}
/// 终端显示宽度:ASCII = 1CJK 等宽字符 = 2Tab = 4
fn char_width(c: char) -> usize {
if c == '\t' {
return 4;
}
if c.is_ascii() {
return 1;
}
let cp = c as u32;
// 宽字符区段
if (0x1100..=0x115F).contains(&cp) // Hangul Jamo
|| (0x2329..=0x232A).contains(&cp) // <>
|| (0x2E80..=0xA4CF).contains(&cp) // CJK Radicals .. CJK
|| (0xA960..=0xA97C).contains(&cp) // Hangul Jamo Extended-A
|| (0xAC00..=0xD7A3).contains(&cp) // Hangul Syllables
|| (0xF900..=0xFAFF).contains(&cp) // CJK Compatibility
|| (0xFE10..=0xFE6F).contains(&cp) // CJK Compatibility Forms
|| (0xFF01..=0xFF60).contains(&cp) // Fullwidth Forms
|| (0xFFE0..=0xFFE6).contains(&cp) // Fullwidth Signs
|| (0x1F300..=0x1F64F).contains(&cp) // Emoticons
|| (0x20000..=0x2FFFF).contains(&cp) // CJK Extension B+
|| (0x30000..=0x3FFFF).contains(&cp) // CJK Extension G+
|| cp >= 0x1F000
// Emoji / Supplemental
{
return 2;
}
1
}
fn display_width(s: &str) -> usize {
s.chars().map(char_width).sum()
}
// 测试
#[cfg(test)]
mod tests {
use super::*;
use crate::core::source::SourceFile;
use crate::error::definitions::unterminated_string_literal::UnterminatedStringLiteralError;
#[test]
fn display_width_ascii() {
assert_eq!(display_width("hello"), 5);
}
#[test]
fn display_width_cjk() {
assert_eq!(display_width("你好世界"), 8); // 4 chars × 2 width
}
#[test]
fn display_width_mixed() {
assert_eq!(display_width("var 你好"), 3 + 1 + 4); // "var" + " " + "你好" = 3 + 1 + 4 = 8
}
#[test]
fn show_unterminated_string_zh_cn() {
let source = "var x = \"hello\nvar y = \"world\n";
let mut mgr = SourceManager::new();
mgr.add_file(SourceFile::from_string("test.fig", source.to_string()));
let err = UnterminatedStringLiteralError::new(0, 2, 9, 2, 14).with_thrower(crate::thrower!());
let reporter = Reporter::new(core::Lang::zh_CN);
let mut buf = String::new();
reporter.report(&err, &mgr, &mut buf);
println!("{buf}");
}
#[test]
fn show_cjk_source() {
let source = "让 你好 = \"你好世界\n";
let mut mgr = SourceManager::new();
mgr.add_file(SourceFile::from_string("test.fig", source.to_string()));
let err = UnterminatedStringLiteralError::new(0, 1, 8, 1, 15).with_thrower(crate::thrower!());
let reporter = Reporter::new(core::Lang::zh_CN);
let mut buf = String::new();
reporter.report(&err, &mgr, &mut buf);
println!("{buf}");
}
#[test]
fn show_unterminated_string_en_us() {
let source = "var x = \"hello\n";
let mut mgr = SourceManager::new();
mgr.add_file(SourceFile::from_string("test.fig", source.to_string()));
let err = UnterminatedStringLiteralError::new(0, 1, 9, 1, 14).with_thrower(crate::thrower!());
let reporter = Reporter::new(core::Lang::en_US);
let mut buf = String::new();
reporter.report(&err, &mgr, &mut buf);
println!("{buf}");
}
}
+942
View File
@@ -0,0 +1,942 @@
/*
src/lexer/lexer.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
use crate::error::definitions::*;
use crate::error::diagnostic::Diagnostic;
use crate::token::TokenKind;
use crate::token;
pub struct SrcReader<'a> {
source: &'a str,
index: usize,
line: usize,
column: usize,
}
impl<'a> SrcReader<'a> {
pub fn new(source: &'a str) -> Self {
SrcReader {
source,
index: 0,
line: 1,
column: 1,
}
}
pub fn read_char(&mut self) -> Option<char> {
if self.index >= self.source.len() {
return None;
}
let ch = self.source[self.index..].chars().next().unwrap();
self.index += ch.len_utf8();
match ch {
'\n' => {
self.line += 1;
self.column = 1;
}
'\t' => {
self.column += 4;
}
_ => {
self.column += 1;
}
}
Some(ch)
}
pub fn peek_char(&self) -> Option<char> {
if self.index >= self.source.len() {
return None;
}
Some(self.source[self.index..].chars().next().unwrap())
}
pub fn is_eof(&self) -> bool {
self.index >= self.source.len()
}
}
pub struct Lexer<'a> {
file_name: &'a str,
file_id: usize,
reader: SrcReader<'a>,
is_eof: bool,
}
impl<'a> Lexer<'a> {
pub fn new(file_name: &'a str, file_id: usize, source: &'a str) -> Self {
Lexer {
file_name,
file_id,
reader: SrcReader::new(source),
is_eof: false,
}
}
pub fn next_token(&mut self) -> Result<token::Token, Box<dyn Diagnostic>> {
if self.is_eof {
panic!("Lexer has reached EOF, cannot read more tokens.");
} else if self.reader.is_eof() {
self.is_eof = true;
return Ok(self.make_eof_token());
}
let line = self.reader.line;
let column = self.reader.column;
let ch = self.reader.read_char().unwrap();
match ch {
' ' | '\t' | '\n' => self.next_token(),
c if c.is_alphabetic() || c == '_' => self.parse_identifier(c),
'"' => self.parse_string(line, column),
'r' if self.reader.peek_char().is_some_and(|c| c == '"') => self.parse_raw_string(),
/* 二进制、八进制、十六进制数字 严格 0b 0o 0x小写,具体数字部分字母大写 */
'0' if self.reader.peek_char().is_some_and(|c| c == 'b') => self.parse_binint(),
'0' if self.reader.peek_char().is_some_and(|c| c == 'o') => self.parse_octint(),
'0' if self.reader.peek_char().is_some_and(|c| c == 'x') => self.parse_hexint(),
c if c.is_ascii_digit() => self.parse_number(c),
'/' if self.reader.peek_char().is_some_and(|c| c == '/') => {
self.skip_line_comment();
self.next_token()
}
'/' if self.reader.peek_char().is_some_and(|c| c == '*') => {
self.parse_block_comment(line, column)
}
c if token::lookup_operator(&c.to_string()).is_some() => self.parse_operator(c),
_ => Err(Box::new(
unexpected_character::UnexpectedCharacterError::new(self.file_id, ch, line, column),
)),
}
}
fn parse_identifier(&mut self, first: char) -> Result<token::Token, Box<dyn Diagnostic>> {
let mut tok = token::Token::new(
TokenKind::Identifier,
self.reader.index - first.len_utf8(),
1,
);
let mut lexeme = String::from(first);
while self
.reader
.peek_char()
.is_some_and(|c| c.is_alphanumeric() || c == '_')
{
tok.length += 1;
lexeme.push(self.reader.read_char().unwrap());
}
if let Some(keyword_type) = token::lookup_keyword(&lexeme) {
Ok(token::Token::new(keyword_type, tok.index, tok.length))
} else {
Ok(tok)
}
}
fn parse_string(
&mut self,
start_line: usize,
start_column: usize,
) -> Result<token::Token, Box<dyn Diagnostic>> {
let mut tok = token::Token::new(token::TokenKind::String, self.reader.index - 1, 1);
let mut terminated = false;
let mut end_line = start_line;
let mut end_column = start_column + 1;
while let Some(c) = self.reader.peek_char() {
match c {
'"' => {
end_line = self.reader.line;
end_column = self.reader.column + 1;
tok.length += 1;
self.reader.read_char();
terminated = true;
break;
}
'\\' => {
tok.length += 1;
self.reader.read_char();
if let Some(esc) = self.reader.read_char() {
tok.length += 1;
match esc {
'n' | 't' | 'r' | '\\' | '"' => {}
_ => {
return self.diag_newline(Box::new(
invalid_escape_sequence::InvalidEscapeSequenceError::new(
self.file_id,
self.reader.line,
self.reader.column - 2,
esc.to_string(),
),
));
}
}
} else {
break;
}
}
_ => {
end_line = self.reader.line;
end_column = self.reader.column;
tok.length += 1;
self.reader.read_char();
}
}
}
if !terminated {
return self.diag_newline(Box::new(
unterminated_string_literal::UnterminatedStringLiteralError::new(
self.file_id,
start_line,
start_column,
end_line,
end_column,
),
));
}
Ok(tok)
}
fn parse_raw_string(&mut self) -> Result<token::Token, Box<dyn Diagnostic>> {
let line = self.reader.line;
let column = self.reader.column;
self.reader.read_char(); // 跳过 '"'
match self.parse_string(line, column) {
Ok(mut tok) => {
tok.kind = token::TokenKind::RawString;
Ok(tok)
}
Err(e) => Err(e),
}
}
fn parse_number(&mut self, first: char) -> Result<token::Token, Box<dyn Diagnostic>> {
let mut tok = token::Token::new(TokenKind::Number, self.reader.index - first.len_utf8(), 1);
if first == '0' && self.reader.peek_char().is_some_and(|c| c.is_ascii_digit()) {
return self.diag_newline(Box::new(
invalid_number_literal::InvalidNumberLiteralError::new(
self.file_id,
self.reader.line,
self.reader.column - 1,
self.reader.column,
),
));
}
self.read_digits(&mut tok);
if self.reader.peek_char().is_some_and(|c| c == '.') {
tok.length += 1;
self.reader.read_char();
self.read_digits(&mut tok);
}
if self
.reader
.peek_char()
.is_some_and(|c| c == 'e' || c == 'E')
{
tok.length += 1;
self.reader.read_char();
if self
.reader
.peek_char()
.is_some_and(|c| c == '+' || c == '-')
{
tok.length += 1;
self.reader.read_char();
}
match self.reader.peek_char() {
Some('0') => {
return self.diag_newline(Box::new(
invalid_number_literal::InvalidNumberLiteralError::new(
self.file_id,
self.reader.line,
self.reader.column,
self.reader.column + 1,
),
));
}
Some(c) if c.is_ascii_digit() => {
self.read_digits(&mut tok);
}
_ => {
return self.diag_newline(Box::new(
invalid_number_literal::InvalidNumberLiteralError::new(
self.file_id,
self.reader.line,
self.reader.column,
self.reader.column + 1,
),
));
}
}
}
Ok(tok)
}
fn parse_binint(&mut self) -> Result<token::Token, Box<dyn Diagnostic>> {
let start_column = self.reader.column - 1;
let mut tok = token::Token::new(TokenKind::BinInt, self.reader.index - 1, 2);
self.reader.read_char(); // 跳过 b
while let Some(c) = self.reader.peek_char() {
match c {
'0' | '1' => {
tok.length += 1;
self.reader.read_char();
}
'2'..='9' => {
return self.diag_newline(Box::new(
invalid_number_literal::InvalidNumberLiteralError::new(
self.file_id,
self.reader.line,
start_column,
self.reader.column,
),
))
}
_ => {
break;
}
}
}
Ok(tok)
}
fn parse_octint(&mut self) -> Result<token::Token, Box<dyn Diagnostic>> {
let start_column = self.reader.column - 1;
let mut tok = token::Token::new(TokenKind::OctInt, self.reader.index - 1, 2);
self.reader.read_char(); // 跳过 o
while let Some(c) = self.reader.peek_char() {
match c {
'0'..='8' => {
tok.length += 1;
self.reader.read_char();
}
'9' => {
return self.diag_newline(Box::new(
invalid_number_literal::InvalidNumberLiteralError::new(
self.file_id,
self.reader.line,
start_column,
self.reader.column,
),
))
}
_ => {
break;
}
}
}
Ok(tok)
}
fn parse_hexint(&mut self) -> Result<token::Token, Box<dyn Diagnostic>> {
let start_column = self.reader.column - 1;
let mut tok = token::Token::new(TokenKind::HexInt, self.reader.index - 1, 2);
self.reader.read_char(); // 跳过 x
while let Some(c) = self.reader.peek_char() {
match c {
'0'..='9' | 'A'..='F' => {
tok.length += 1;
self.reader.read_char();
}
'G'..='Z' => {
return self.diag_newline(Box::new(
invalid_number_literal::InvalidNumberLiteralError::new(
self.file_id,
self.reader.line,
start_column,
self.reader.column,
),
))
}
_ => {
break;
}
}
}
Ok(tok)
}
fn parse_operator(&mut self, first: char) -> Result<token::Token, Box<dyn Diagnostic>> {
let start = self.reader.index - 1; // first 一定是 ASCII1 字节
let rest = &self.reader.source[start..];
for len in [2, 1] {
if let Some(prefix) = rest.get(..len) {
if let Some((kind, op_len)) = token::lookup_operator(prefix) {
for _ in 1..op_len {
self.reader.read_char();
}
return Ok(token::Token::new(kind, start, op_len as u32));
}
}
}
unreachable!("dispatcher already confirmed first char is an operator")
}
fn read_digits(&mut self, tok: &mut token::Token) {
while self.reader.peek_char().is_some_and(|c| c.is_ascii_digit()) {
tok.length += 1;
self.reader.read_char();
}
}
fn diag_newline(
&mut self,
diagnostic: Box<dyn Diagnostic>,
) -> Result<token::Token, Box<dyn Diagnostic>> {
while !self.reader.is_eof() {
let ch = self.reader.read_char().unwrap();
if ch == '\n' {
break;
}
}
Err(diagnostic)
}
fn skip_line_comment(&mut self) {
self.reader.read_char();
while let Some(c) = self.reader.peek_char() {
if c == '\n' {
break;
}
self.reader.read_char();
}
}
fn parse_block_comment(
&mut self,
start_line: usize,
start_column: usize,
) -> Result<token::Token, Box<dyn Diagnostic>> {
self.reader.read_char();
let mut end_line = start_line;
let mut end_column = start_column;
while let Some(c) = self.reader.peek_char() {
end_line = self.reader.line;
end_column = self.reader.column;
if c == '*' {
self.reader.read_char();
if self.reader.peek_char().is_some_and(|c| c == '/') {
self.reader.read_char();
return self.next_token();
}
} else {
self.reader.read_char();
}
}
self.diag_newline(Box::new(unterminated_comment::UnterminatedCommentError::new(
self.file_id,
start_line,
start_column,
end_line,
end_column,
)))
}
fn make_eof_token(&self) -> token::Token {
token::Token::new(TokenKind::Eof, self.reader.index, 1)
}
fn diag_semicolon(
&mut self,
diagnostic: Box<dyn Diagnostic>,
) -> Result<token::Token, Box<dyn Diagnostic>> {
while !self.reader.is_eof() {
let ch = self.reader.read_char().unwrap();
if ch == ';' {
break;
}
}
Err(diagnostic)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core;
use crate::core::source::{SourceFile, SourceManager};
use crate::error::report::Reporter;
use crate::token::TokenKind::*;
use std::string::String as StdString;
fn setup(src: &str) -> (SourceManager, Reporter) {
let mut mgr = SourceManager::new();
mgr.add_file(SourceFile::from_string("test", src.to_string()));
(mgr, Reporter::new(core::Lang::zh_CN))
}
fn lex(source: &str) -> Vec<token::Token> {
let (mgr, reporter) = setup(source);
let mut lexer = Lexer::new("test", 0, source);
let mut tokens = vec![];
loop {
match lexer.next_token() {
Ok(tok) => {
let is_eof = tok.kind == Eof;
tokens.push(tok);
if is_eof {
break;
}
}
Err(e) => {
let mut buf = StdString::new();
reporter.report(e.as_ref(), &mgr, &mut buf);
println!("{buf}");
panic!("Lexer error in test");
}
}
}
tokens
}
fn kinds(tokens: &[token::Token]) -> Vec<token::TokenKind> {
tokens.iter().map(|t| t.kind).collect()
}
#[test]
fn keywords() {
let tokens = lex("var const func if else return true false null");
assert_eq!(
kinds(&tokens),
[Var, Const, Func, If, Else, Return, True, False, Null, Eof]
);
}
#[test]
fn identifiers() {
let tokens = lex("foo _bar 你好 a1 b2_c3");
let ks = kinds(&tokens);
assert_eq!(ks[0], Identifier);
assert_eq!(ks[1], Identifier);
assert_eq!(ks[2], Identifier);
assert_eq!(ks[3], Identifier);
assert_eq!(ks[4], Identifier);
assert_eq!(ks[5], Eof);
}
#[test]
fn numbers() {
let tokens = lex("42 3.14 5e2 0b1010 0o77 0xFF");
assert_eq!(
kinds(&tokens),
[Number, Number, Number, BinInt, OctInt, HexInt, Eof]
);
}
#[test]
fn binint() {
let tokens = lex("0b1010 0b0 0b11111111");
assert_eq!(kinds(&tokens), [BinInt, BinInt, BinInt, Eof]);
}
#[test]
fn octint() {
let tokens = lex("0o777 0o0 0o1234567");
assert_eq!(kinds(&tokens), [OctInt, OctInt, OctInt, Eof]);
}
#[test]
fn hexint() {
let tokens = lex("0xFF 0xDEAD 0xBEEF 0xA0");
assert_eq!(kinds(&tokens), [HexInt, HexInt, HexInt, HexInt, Eof]);
}
#[test]
fn binint_invalid_digit() {
let (mgr, reporter) = setup("0b102");
let mut lexer = Lexer::new("test", 0, "0b102");
let mut buf = StdString::new();
match lexer.next_token() {
Err(e) => reporter.report(e.as_ref(), &mgr, &mut buf),
_ => {}
}
println!("{buf}");
}
#[test]
fn strings() {
let tokens = lex(r#""hello" "world\n""#);
assert_eq!(kinds(&tokens), [String, String, Eof]);
}
#[test]
fn operators_single() {
let tokens = lex("+ - * / % = < > & | ^ ~");
assert_eq!(
kinds(&tokens),
[Plus, Minus, Star, Slash, Percent, Assign, Lt, Gt, Amp, Pipe, Caret, Tilde, Eof]
);
}
#[test]
fn operators_multi() {
let tokens = lex("== != <= >= && || << >> ++ -- **");
assert_eq!(
kinds(&tokens),
[EqEq, NotEq, LtEq, GtEq, AndAnd, OrOr, LtLt, GtGt, PlusPlus, MinusMinus, StarStar, Eof]
);
}
#[test]
fn operators_assign() {
let tokens = lex("+= -= *= /= %= ^=");
assert_eq!(
kinds(&tokens),
[PlusAssign, MinusAssign, StarAssign, SlashAssign, PercentAssign, CaretAssign, Eof]
);
}
#[test]
fn operators_longest_match() {
// (x = 42) 中 = 不能和 == 混淆
let tokens = lex("x = 42 x == 42");
assert_eq!(
kinds(&tokens),
[Identifier, Assign, Number, Identifier, EqEq, Number, Eof]
);
}
#[test]
fn operators_pair() {
let tokens = lex("() {} []");
assert_eq!(
kinds(&tokens),
[LParen, RParen, LBrace, RBrace, LBracket, RBracket, Eof]
);
}
#[test]
fn operators_closing_only() {
let tokens = lex(") } ]");
assert_eq!(kinds(&tokens), [RParen, RBrace, RBracket, Eof]);
}
#[test]
fn operators_delimiters() {
let tokens = lex("() {} [] , ; : . -> => ? @ # $");
assert_eq!(
kinds(&tokens),
[
LParen, RParen,
LBrace, RBrace,
LBracket, RBracket,
Comma, Semicolon, Colon, Dot,
Arrow, FatArrow,
Question, At, Hash, Dollar,
Eof,
]
);
}
#[test]
fn unterminated_string_errors() {
let source = "\"hello\n";
let (mgr, reporter) = setup(source);
let mut lexer = Lexer::new("test", 0, source);
let mut buf = StdString::new();
match lexer.next_token() {
Err(e) => reporter.report(e.as_ref(), &mgr, &mut buf),
_ => {}
}
println!("{buf}");
let tok = lexer.next_token().unwrap();
assert_eq!(tok.kind, Eof);
}
#[test]
fn line_comment() {
let tokens = lex("42 // this is a comment\n99");
assert_eq!(kinds(&tokens), [Number, Number, Eof]);
}
#[test]
fn block_comment() {
let tokens = lex("42 /* block */ 99");
assert_eq!(kinds(&tokens), [Number, Number, Eof]);
}
#[test]
fn block_comment_multiline() {
let tokens = lex("42 /* line 1\nline 2\n*/ 99");
assert_eq!(kinds(&tokens), [Number, Number, Eof]);
}
#[test]
fn block_comment_unterminated() {
let source = "42 /* never closed";
let (mgr, reporter) = setup(source);
let mut lexer = Lexer::new("test", 0, source);
let mut buf = StdString::new();
while let Ok(tok) = lexer.next_token() {
if tok.kind == Eof { break; }
}
// error was reported via diag_newline, which recovers to newline
// verify the error system captured it (we don't assert output, just no panic)
}
#[test]
fn comment_inside_code() {
let source = r#"
var x = 1 // inline comment
/* multi
line */
var y = 2
"#;
let (mgr, reporter) = setup(source);
let mut lexer = Lexer::new("test", 0, source);
loop {
match lexer.next_token() {
Ok(tok) => {
if tok.kind == Eof { break; }
}
Err(_) => {}
}
}
}
#[test]
fn edge_empty() {
// 空源码 → 只有 Eof
let tokens = lex("");
assert_eq!(kinds(&tokens), [Eof]);
}
#[test]
fn edge_whitespace_only() {
// 纯空白
let tokens = lex(" \n \t \n ");
assert_eq!(kinds(&tokens), [Eof]);
}
#[test]
fn edge_single_chars() {
let tokens = lex("+ - * / % = < > & | ^ ~ ! . , ; : ? @ # $ ( ) { } [ ]");
assert_eq!(kinds(&tokens).len(), 28); // 27 tokens + Eof
}
#[test]
fn edge_eof_after_token_start() {
let (_, reporter) = setup("\"eof");
let mut lexer = Lexer::new("test", 0, "\"eof");
assert!(lexer.next_token().is_err());
assert_eq!(lexer.next_token().unwrap().kind, Eof);
}
#[test]
fn edge_consecutive_operators() {
let tokens = lex("+-*/%&&||==!=<><=>=->=>::++--**+=-=*=/=%=^=");
assert!(kinds(&tokens).len() > 20);
}
#[test]
fn edge_operator_at_eof() {
let tokens = lex("42+");
assert_eq!(kinds(&tokens), [Number, Plus, Eof]);
}
#[test]
fn edge_line_comment_at_eof() {
let tokens = lex("42// no newline");
assert_eq!(kinds(&tokens), [Number, Eof]);
}
#[test]
fn edge_block_comment_at_eof() {
let mut lexer = Lexer::new("test", 0, "42/* no close");
let mut kinds = vec![];
loop {
match lexer.next_token() {
Ok(tok) => {
kinds.push(tok.kind);
if tok.kind == Eof { break; }
}
Err(_) => {}
}
}
assert_eq!(&kinds, &[Number, Eof]);
}
#[test]
fn edge_zero_at_eof() {
let tokens = lex("x >= 0");
assert_eq!(kinds(&tokens), [Identifier, GtEq, Number, Eof]);
}
#[test]
fn edge_empty_prefix_number() {
let tokens = lex("0b 0x 0o");
assert_eq!(kinds(&tokens), [BinInt, HexInt, OctInt, Eof]);
}
#[test]
fn edge_empty_raw_string() {
let mut lexer = Lexer::new("test", 0, r###"r"""###);
let tok = lexer.next_token().unwrap();
println!("{:?}", tok.kind);
}
#[test]
fn edge_star_not_comment() {
let tokens = lex("2 * 3");
assert_eq!(kinds(&tokens), [Number, Star, Number, Eof]);
}
#[test]
fn edge_slash_not_comment() {
let tokens = lex("2 / 3");
assert_eq!(kinds(&tokens), [Number, Slash, Number, Eof]);
}
#[test]
fn edge_block_comment_star_only() {
let source = "42 /* * * *";
let mut lexer = Lexer::new("test", 0, source);
let mut tokens = vec![];
loop {
match lexer.next_token() {
Ok(tok) => {
let is_eof = tok.kind == Eof;
tokens.push(tok.kind);
if is_eof { break; }
}
Err(_) => {} // unterminated comment, recovered
}
}
assert_eq!(&tokens, &[Number, Eof]);
}
#[test]
fn stress_1000_lines() {
let mut source = StdString::new();
// 生成约 1000 行覆盖全 token 类型的源码
for i in 0..60 {
let hex = format!("0x{:X}", i * 7);
let oct = format!("0o{:o}", i);
let bin = format!("0b{:b}", i % 256);
source.push_str(&format!(
"var v{i} = {i}\n\
const c{i} = {i}.{i}\n\
let s{i} = \"str {i} 你好\"\n\
// line comment {i}\n\
func f{i}(a{i}, b{i}) {{\n\
/* block comment {i} */\n\
if a{i} != {i} {{\n\
return a{i} + b{i} * {i} - {hex} | {oct}\n\
}} else {{\n\
return c{i} >= {i} && v{i} <= {i} || f{i}(a{i}, b{i})\n\
}}\n\
}}\n\
v{i} += {i}\n\
v{i} **= 2\n\
let t{i} = true && !false\n\
let n{i} = !{i}.0 + 0xFF\n\
let arr{i} = [? # $ @ -> =>]\n\
"
));
}
// 添加故意错误行(错误恢复验证)
source.push_str("var bad = \"never close this\n");
source.push_str("/* unterminated block comment\n");
source.push_str("var after_comment = 42\n");
let (mgr, reporter) = setup(&source);
let mut lexer = Lexer::new("stress", 0, &source);
let mut tokens = 0usize;
let mut errors = 0usize;
loop {
match lexer.next_token() {
Ok(tok) => {
tokens += 1;
if tok.kind == Eof { break; }
}
Err(e) => {
errors += 1;
let mut buf = StdString::new();
reporter.report(e.as_ref(), &mgr, &mut buf);
println!("{buf}");
}
}
}
let lines = source.lines().count();
println!(
"stress: {} lines => {} tokens, {} errors recovered",
lines, tokens, errors
);
assert!(tokens > 2000, "expected >2000 tokens, got {tokens}");
assert!(lines > 800, "expected >800 lines, got {lines}");
}
#[test]
fn stress_test() {
let source = r#"
var 你好 = "hello 世界"
const MAX = 0xFF
func fib(n) {
if n <= 1 {
return n
}
return fib(n - 1) + fib(n - 2)
}
let x = 42 + 3.14
let y = x * 2 - 1
let flag = true && !false
let bits = 0b1010 | 0o77
let result = (x >= 0) && (x != 42)
x += 1
y -= 1
x **= 2
-> => ? @ # $
"#;
let (mgr, reporter) = setup(source);
let mut lexer = Lexer::new("test", 0, source);
let mut count = 0usize;
loop {
match lexer.next_token() {
Ok(tok) => {
let text: StdString = source[tok.index..]
.chars()
.take(tok.length as usize)
.collect();
count += 1;
println!("{:>3} {:?} '{}'", count, tok.kind, text);
if tok.kind == Eof {
break;
}
}
Err(e) => {
let mut buf = StdString::new();
reporter.report(e.as_ref(), &mgr, &mut buf);
println!("{buf}");
}
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
/*
src/lexer/mod.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
pub mod lexer;
pub use lexer::Lexer as Lexer;
+14
View File
@@ -0,0 +1,14 @@
/*
src/main.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
mod core;
mod error;
mod token;
mod lexer;
fn main() {
println!("Fig Language Compiler v0.6.0");
}
+231
View File
@@ -0,0 +1,231 @@
/*
src/token/mod.rs
Part of The Fig Project, under the MIT License.
Copyright (c) 2026, PuqiAR (im@puqiar.top) All rights reserved.
See LICENSE for details.
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenKind {
Eof,
// 标识符
Identifier,
// 字面量
String,
RawString, // r"raw string"
Number, // 123, 3.1415, 100e10 (等同 100e+10) ...
BinInt, // 0b1010
OctInt, // 0o77
HexInt, // 0xFF
// 关键字
Var,
Const,
Func,
Struct,
Interface,
Impl,
Enum,
If,
Else,
For,
While,
Return,
Break,
Continue,
Import,
New,
As,
Is,
True,
False,
Null,
And,
Or,
Not,
Public,
// 分隔符
LParen, // (
RParen, // )
LBrace, // {
RBrace, // }
LBracket, // [
RBracket, // ]
Comma, // ,
Dot, // .
Semicolon, // ;
Colon, // :
Arrow, // ->
FatArrow, // =>
Question, // ?
At, // @
Hash, // #
Dollar, // $
// 运算符
Plus, // +
Minus, // -
Star, // *
Slash, // /
Percent, // %
StarStar, // **
PlusPlus, // ++
MinusMinus, // --
// 赋值运算符
Assign, // =
PlusAssign, // +=
MinusAssign, // -=
StarAssign, // *=
SlashAssign, // /=
PercentAssign, // %=
CaretAssign, // ^=
// 比较运算符
EqEq, // ==
NotEq, // !=
Lt, // <
Gt, // >
LtEq, // <=
GtEq, // >=
// 逻辑运算符
AndAnd, // &&
OrOr, // ||
// 位运算符
Amp, // &
Pipe, // |
Caret, // ^
Tilde, // ~
Bang, // !
LtLt, // <<
GtGt, // >>
}
/// Token 实例
#[derive(Debug, Clone)]
pub struct Token {
pub kind: TokenKind,
pub index: usize,
pub length: u32,
}
impl Token
{
pub fn new(kind: TokenKind, index: usize, length: u32) -> Self
{
Token
{
kind,
index,
length
}
}
}
/// 运算符打表,按字符串长度降序保证最长匹配优先。
pub static OPERATORS: &[(&str, TokenKind)] = &[
("->", TokenKind::Arrow),
("=>", TokenKind::FatArrow),
("**", TokenKind::StarStar),
("*=", TokenKind::StarAssign),
("++", TokenKind::PlusPlus),
("+=", TokenKind::PlusAssign),
("--", TokenKind::MinusMinus),
("-=", TokenKind::MinusAssign),
("/=", TokenKind::SlashAssign),
("%=", TokenKind::PercentAssign),
("^=", TokenKind::CaretAssign),
("==", TokenKind::EqEq),
("!=", TokenKind::NotEq),
("<=", TokenKind::LtEq),
(">=", TokenKind::GtEq),
("&&", TokenKind::AndAnd),
("||", TokenKind::OrOr),
("<<", TokenKind::LtLt),
(">>", TokenKind::GtGt),
("+", TokenKind::Plus),
("-", TokenKind::Minus),
("*", TokenKind::Star),
("/", TokenKind::Slash),
("%", TokenKind::Percent),
("=", TokenKind::Assign),
("<", TokenKind::Lt),
(">", TokenKind::Gt),
("&", TokenKind::Amp),
("|", TokenKind::Pipe),
("^", TokenKind::Caret),
("~", TokenKind::Tilde),
("!", TokenKind::Bang),
("(", TokenKind::LParen),
(")", TokenKind::RParen),
("{", TokenKind::LBrace),
("}", TokenKind::RBrace),
("[", TokenKind::LBracket),
("]", TokenKind::RBracket),
(",", TokenKind::Comma),
(".", TokenKind::Dot),
(";", TokenKind::Semicolon),
(":", TokenKind::Colon),
("?", TokenKind::Question),
("@", TokenKind::At),
("#", TokenKind::Hash),
("$", TokenKind::Dollar),
];
/// 遍历 OPERATORS,返回第一个匹配的 (TokenKind, 长度)。
pub fn lookup_operator(prefix: &str) -> Option<(TokenKind, usize)> {
for (s, kind) in OPERATORS {
if prefix.starts_with(s) {
return Some((*kind, s.len()));
}
}
None
}
pub fn lookup_keyword(word: &str) -> Option<TokenKind> {
match word {
"var" => Some(TokenKind::Var),
"const" => Some(TokenKind::Const),
"func" => Some(TokenKind::Func),
"struct" => Some(TokenKind::Struct),
"interface" => Some(TokenKind::Interface),
"impl" => Some(TokenKind::Impl),
"enum" => Some(TokenKind::Enum),
"if" => Some(TokenKind::If),
"else" => Some(TokenKind::Else),
"for" => Some(TokenKind::For),
"while" => Some(TokenKind::While),
"return" => Some(TokenKind::Return),
"break" => Some(TokenKind::Break),
"continue" => Some(TokenKind::Continue),
"import" => Some(TokenKind::Import),
"new" => Some(TokenKind::New),
"as" => Some(TokenKind::As),
"is" => Some(TokenKind::Is),
"true" => Some(TokenKind::True),
"false" => Some(TokenKind::False),
"null" => Some(TokenKind::Null),
"and" => Some(TokenKind::And),
"or" => Some(TokenKind::Or),
"not" => Some(TokenKind::Not),
"public" => Some(TokenKind::Public),
_ => None,
}
}