Compare commits
41 Commits
dd0b2904ba
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 68e6552d07 | |||
| 82c7c7178f | |||
| 9f7fe79a2e | |||
| 0f5304001c | |||
| 680197aafe | |||
| 4f87078a87 | |||
| 9338c21449 | |||
| 98de782760 | |||
| fafa2b4946 | |||
| 570a87c3cd | |||
| e1d9812f92 | |||
| 6bcc98bdb3 | |||
| 91b5a0e384 | |||
| c0eacfd236 | |||
| 51a939ac45 | |||
| 0f635ccf2b | |||
| 90448006ff | |||
| 91e4eb734e | |||
| 6dbecbbdc0 | |||
| 1fe9ccf7ea | |||
| bb23ddf9fa | |||
| 12dc31a6c0 | |||
| a0fb8cdffb | |||
| b7bb889676 | |||
| 852dd27836 | |||
| abdb1d2fb0 | |||
| eb20993e27 | |||
| 2631f76da1 | |||
| f2e899c7a7 | |||
| c81da16dfb | |||
| 663fe39070 | |||
| 6b75e028ff | |||
| 878157c2fc | |||
| 35e479fd05 | |||
| 51e831cc6a | |||
| 35b98c4d7f | |||
| 877253cbbc | |||
| cfcdfde170 | |||
| 5e75402b43 | |||
| 642ce66f75 | |||
| 58212a3715 |
@@ -0,0 +1,15 @@
|
||||
# Xmake cache
|
||||
.xmake/
|
||||
build/
|
||||
|
||||
# MacOS Cache
|
||||
.DS_Store
|
||||
|
||||
.vscode
|
||||
.VSCodeCounter
|
||||
|
||||
/test.fig
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
@@ -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"
|
||||
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "fig"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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));
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -0,0 +1,5 @@
|
||||
out
|
||||
dist
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
@@ -0,0 +1,5 @@
|
||||
import { defineConfig } from '@vscode/test-cli';
|
||||
|
||||
export default defineConfig({
|
||||
files: 'out/test/**/*.test.js',
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
src/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/eslint.config.mjs
|
||||
**/*.map
|
||||
**/*.ts
|
||||
**/.vscode-test.*
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
# fig-vscode README
|
||||
|
||||
Fuck.
|
||||
@@ -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",
|
||||
},
|
||||
}];
|
||||
@@ -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 |
@@ -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 |
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "'" }
|
||||
]
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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_]*"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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. */
|
||||
}
|
||||
}
|
||||
@@ -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 doesn’t 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.
|
||||
@@ -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.
|
||||
|
||||
|
After Width: | Height: | Size: 9.7 KiB |
@@ -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 |
|
After Width: | Height: | Size: 9.7 KiB |
@@ -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 |
@@ -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)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**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%
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**Fig** 是一门动态类型与静态类型注解混合的编程语言,采用基于引用的值语义。
|
||||
|
||||
> **v0.6.0 — Rust 重写进行中。**
|
||||
|
||||
## 进度
|
||||
|
||||
```
|
||||
Lexer ████████████████████ 100% (43 测试, 全部通过)
|
||||
Parser ░░░░░░░░░░░░░░░░░░░░ 0%
|
||||
Analyzer ░░░░░░░░░░░░░░░░░░░░ 0%
|
||||
Compiler ░░░░░░░░░░░░░░░░░░░░ 0%
|
||||
VM ░░░░░░░░░░░░░░░░░░░░ 0%
|
||||
LSP ░░░░░░░░░░░░░░░░░░░░ 0%
|
||||
```
|
||||
@@ -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());
|
||||
|
||||
// 编译器 ID:macOS 上用 clang/LLVM,Linux 上用 GCC,Windows 上用 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)
|
||||
}
|
||||
|
After Width: | Height: | Size: 633 KiB |
@@ -0,0 +1,24 @@
|
||||
# Fig 语言简介
|
||||
|
||||
## 概述
|
||||
Fig 是一门动态类型、解释执行的编程语言,专注于简洁语法和实用的语言特性。它采用树遍历解释器架构,支持多种编程范式。
|
||||
|
||||
## 实际观察到的特性
|
||||
1. **解释执行**:基于 AST 的树遍历解释器,无编译步骤
|
||||
2. **动态类型系统**:运行时类型检查,支持类型注解但不强制
|
||||
3. **混合范式**:支持函数式、面向对象和命令式风格
|
||||
4. **模块系统**:支持代码组织和复用
|
||||
5. **内置类型**:整数、浮点数、字符串、列表、映射等
|
||||
6. **垃圾回收**:基于引用计数的自动内存管理
|
||||
|
||||
## 语言设计特点
|
||||
- **渐进类型**:支持类型注解但不强制,兼顾灵活性和可读性
|
||||
- **一等公民函数**:函数可以作为参数传递和返回
|
||||
- **闭包支持**:完整的词法作用域和闭包
|
||||
- **错误处理**:异常机制和 try-catch 结构
|
||||
- **可变与不可变**:const/var 区分,平衡安全与灵活
|
||||
|
||||
## 目标应用场景
|
||||
- 脚本编写和自动化任务
|
||||
- 教育用途和学习编程
|
||||
- 配置语言和DSL
|
||||
@@ -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"`
|
||||
@@ -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());
|
||||
```
|
||||
@@ -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)
|
||||
{
|
||||
// 处理另一种错误
|
||||
}
|
||||
```
|
||||
@@ -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;
|
||||
});
|
||||
```
|
||||
@@ -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)模式请规范使用:
|
||||
|
||||
示例:定义
|
||||
```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实现
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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 亮
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 = 1,CJK 等宽字符 = 2,Tab = 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}");
|
||||
}
|
||||
}
|
||||
@@ -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 一定是 ASCII,1 字节
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||