尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Slang 语句 AST 参考指南:Stmt 类层次结构、解析流程与诊断错误码全解析

Slang 语句 AST 参考指南:Stmt 类层次结构、解析流程与诊断错误码全解析 Slang 语句 AST 参考指南Stmt 类层次结构、解析流程与诊断错误码全解析【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang导读本文是 Slang 编译器源码中语句StatementAST 体系的权威参考指南面向需要阅读或修改解析器parser、语义检查器checker或 IR 降级IR lowering代码的贡献者。文章完整梳理了Stmt的所有具体子类、Parser::ParseStatement的调度与两阶段解析策略、switch/for/__target_switch/defer等语句的底层解析行为以及与之配套的E20001、E20004、E20101、E29110、E36105、E36109六个用户可见诊断错误码。读完本文你将能够在源码中快速定位任意语句节点的定义、解析入口与相关测试并理解 HLSL 输入下for循环变量泄漏等微妙行为。文档来源与定位本文主体对应仓库中的自动生成文档 docs/generated/design/ast-reference/statements.md并依据 docs/generated/design/_meta/gap-intake/ast-reference/statements.md.gap-intake.md 中记录的 8 项缺口gap处理结果将 7 项已修复的文档缺口诊断码、defer可观察语义、switch单作用域结论等与 1 项暂缓缺口HLSL 输入语言选择机制一并整合进正文同时结合 source/slang/slang-parser.cpp、source/slang/slang-ast-stmt.h 等源码与docs/generated/tests/design/ast-reference/statements/下的 bundle 测试做交叉印证。语句 AST 的整体框架类的声明位置语句Statement类全部声明在 source/slang/slang-ast-stmt.h 中而所有语句的抽象根Stmt并不在该文件里——它是 source/slang/slang-ast-base.h 中的抽象基类之一与Expr、Decl等并列。文档对Stmt本身的说明位于 base.md。解析入口语句的解析入口是Parser::ParseStatementslang-parser.cpp它是一个基于关键字前瞻keyword-lookahead的调度器根据下一个 token 选择对应的Parse*Statement/parse*Stmt辅助函数Parser::parseBlockStatementslang-parser.cpp则负责解析{ ... }代码块。两者都接收一个AllowCaseDefaultStatements参数这正是case/default写在switch体外时能先被解析成节点、再被诊断而非直接报unexpected token的原因参见 keywords-and-builtins.md。两阶段解析策略函数体并不会在解析声明的同时被解析解析器运行在ParsingStage::Decl与ParsingStage::Body两个阶段之一ParsingStage枚举位于 slang-parser.cpp。parseOptBodyslang-parser.cpp在遇到{限定的函数体时不递归进入parseBlockStatement而是将其记录为一个UnparsedStmtparseUnparsedStmtslang-parser.cpp稍后以stage ParsingStage::Body重新进入把这些 token 真正变成BlockStmt。整体策略参见 02-parse-ast.md。家族层次结构Family hierarchy语句节点沿两个轴划分其一是否是一个ScopeStmt分组结构基类只有部分子类真正持有ScopeDecl其二是否可以作为break循环与switch或continue仅循环的目标。此外还有一组ChildStmt保存对某个外围BreakableStmt的引用。BreakableStmt携带一个UniqueStmtIDNode* uniqueIDChildStmt通过targetOuterStmtID引用它。UniqueStmtIDNode在头文件中被声明为Decl子类仅仅是为了序列化的便利——它并不是真正的声明虽然不在Stmt层次内、也不被当作语句解析但由于头文件用FIDDLE()宏声明它它仍会出现在下文## Nodes表中作为辅助节点。节点总表Nodes下表是全部语句节点的速查表字段与文法一应俱全ClassParentKey fieldsGrammarSummarySeqStmtStmtstmts: ListStmt*(none)扁平的语句序列用于在只允许单条语句的位置如{ ... }块的若干语句填充多条语句。LabelStmtStmtlabel: Token,innerStmt: Stmt*labeled stmtlabel:后跟一条语句。BlockStmtScopeStmtbody: Stmt*,closingSourceLoc: SourceLocblock{ ... }为块级声明引入一个ScopeDecl。UnparsedStmtStmttokens: ListToken,sourceLanguage: SourceLanguage,currentScope: Scope*,outerScope: Scope*(none)在ParsingStage::Decl阶段以原始 token 捕获的{ ... }函数体按需重新解析。EmptyStmtStmt(无附加状态)empty stmt裸;。DiscardStmtStmt(无附加状态)discarddiscard片元着色器像素丢弃。DeclStmtStmtdecl: DeclBase*decl stmt在语句位置使用声明例如函数体内的int x 1;。IfStmtStmtpredicate: Expr*,positiveStatement: Stmt*,negativeStatement: Stmt*,afterLoc: SourceLocifif (...) ... else ...也是if (let x ...)脱糖后的节点。SwitchStmtBreakableStmtcondition: Expr*,body: Stmt*switchswitch (cond) { ... }。TargetCaseStmtChildStmtcapability: int32_t,capabilityToken: Token,body: Stmt*__target_switch__target_switch内的case capability:或default:capability保存一个CapabilityName代码。TargetSwitchStmtBreakableStmttargetCases: ListTargetCaseStmt*__target_switch按 capability 集合静态分发。StageSwitchStmtTargetSwitchStmt(继承)__stage_switch按管线阶段静态分发。IntrinsicAsmStmtStmtasmText: String,args: ListExpr*__intrinsic_asm内联 intrinsic 汇编语句供核心模块 intrinsic 使用。CaseStmtCaseStmtBaseexpr: Expr*,exprVal: Val*caseswitch内的case expr:。DefaultStmtCaseStmtBase(无附加状态)defaultswitch内的default:。GpuForeachStmtScopeStmtdevice: Expr*,gridDims: Expr*,dispatchThreadID: VarDecl*,kernelCall: Expr*__GPU_FOREACH宿主机侧对网格的计算式 foreach写法为__GPU_FOREACH(device, gridDims, LAMBDA(...) { ... })。ForStmtLoopStmtinitialStatement: Stmt*,sideEffectExpression: Expr*,predicateExpression: Expr*,statement: Stmt*forfor (init; cond; step) body循环变量限定在函数体内作用域。UnscopedForStmtForStmt(继承)for语法与ForStmt相同仅对 HLSL 输入产生——此时循环变量泄漏到外围作用域。WhileStmtLoopStmtpredicate: Expr*,statement: Stmt*whilewhile (cond) body。DoWhileStmtLoopStmtstatement: Stmt*,predicate: Expr*do-whiledo body while (cond);。CompileTimeForStmtScopeStmtvarDecl: VarDecl*,rangeBeginExpr: Expr*,rangeEndExpr: Expr*,body: Stmt*外加检查期填充的rangeBeginVal/rangeEndValIntVal*compile-time for编译期展开的基于范围的循环不产生运行时循环。BreakStmtJumpStmttargetLabel: Tokenbreakbreak可带目标标签。ContinueStmtJumpStmt(继承)continuecontinue与break不同不带标签。ReturnStmtStmtexpression: Expr*returnreturn可带表达式。DeferStmtStmtstatement: Stmt*deferdefer Sstatement保存被延迟的语句。ThrowStmtStmtexpression: Expr*throw可错误函数errorable function中的throw eParseThrowStatement本身不消费尾随;。CatchStmtStmterrorVar: ParamDecl*,tryBody: Stmt*,handleBody: Stmt*do-catchdo { ... } catch (e) { ... }tryBody是被保护体handleBody是处理器errorVar null表示 catch-all。ExpressionStmtStmtexpression: Expr*expression stmt以副作用为目的的表达式f();、a b;。RequireCapabilityStmtStmtrequiredCaps: ListToken__requireCapability__requireCapability(...)语句级 capability 要求作用域限定于所在函数。UniqueStmtIDNodeDecl(无解析状态)(none)综合产生的身份辅助节点为语句提供稳定的唯一 id用于序列化与控制流追踪而非被当作语句解析。重点节点详解Notable nodesBlockStmt 与 SeqStmt作用域从何而来ScopeStmt是所有可能拥有词法作用域的控制流分组的结构基类。它唯一的字段scopeDecl: ScopeDecl*是块内局部声明所挂载的容器声明名字查找从语句出发向外遍历时正是通过它找到这些声明。只有四个解析例程会填充它Parser::parseBlockStatement为BlockStmt填充slang-parser.cppParser::ParseForStatement为有作用域的for填充slang-parser.cppparseGpuForeachStmtslang-parser.cppparseCompileTimeForStmtslang-parser.cpp。SwitchStmt和TargetSwitchStmt通过BreakableStmt继承ScopeStmt但scopeDecl保持为 null——switch的词法作用域取自解析器为其函数体构建的BlockStmtWhileStmt/DoWhileStmt则不引入自己的作用域。由此得出一个用户可见的重要语义整个switch函数体是一个单一作用域。在case 1:下声明的变量在case 2:和default:下依然在作用域内并且会在函数体其余部分遮蔽函数体外同名变量。这是 gap-intake 修复7050023ee030的结论其证据链为slang-parser.cpp 用parseBlockStatement(AllowCaseDefaultStatements::Allow)解析整个switch函数体ParseCaseStmt/ParseDefaultStmt:6592、:6602只读取case expr :/default :因此标签下的声明是同一个BlockStmt作用域内的兄弟节点。bundle 测试 switchstmt-body-block-scope.slang 固化了这一可观察行为default:中的读取解析到case 1:的声明而非外层变量。提示SwitchStmt::body永远是BlockStmt——ParseSwitchStmtslang-parser.cpp用parseBlockStatement解析{ ... }该块的body通常是混合了CaseStmt、DefaultStmt与其他语句的SeqStmt。case 标签并不是其下语句的父节点ParseCaseStmt与ParseDefaultStmtslang-parser.cpp只读取case expr :/default :后续语句作为兄弟留在序列中所以每个CaseStmt/DefaultStmt只是序列里的一个标记。SwitchStmt内的BreakStmt通过BreakableStmt::uniqueID匹配。BlockStmt是最简单的ScopeStmt——一个body为单条Stmt的{ ... }块。SeqStmt则相反是一个没有自身作用域的扁平容器存在的意义就是让多条语句能填入只放得下一条Stmt的槽位。Parser::parseBlockStatement正是这样使用它块中第一条语句直接成为body第二条出现时创建一个SeqStmt并把两条都移进去空块获得EmptyStmt作为body而非 null。另外两处会构建SeqStmtParser::parseIfLetStatementslang-parser.cpp与parseTargetSwitchStmtImplslang-parser.cpp中每个 case 的函数体循环。UnparsedStmt函数体的第一形态UnparsedStmt并非什么奇特构造——它就是函数体在解析第一阶段最普通的表现形式。当解析器在期望函数体的位置遇到{时parseOptBodyslang-parser.cpp不递归进入parseBlockStatement而是把直到匹配}的 token 复制进UnparsedStmt::tokens以合成的文件结束 token 结尾并记录当时生效的两个作用域currentScope与outerScope。像 slang-parser.cpp 处的函数声明解析器这样的调用方会把结果当作该 decl 的body。被捕获的作用域正是延迟deferral安全性的关键当函数体最终被需要时parseUnparsedStmtslang-parser.cpp在一个以stage ParsingStage::Body配置的全新Parser上重新安装这两个作用域并调用parseBlockStatement于是函数体在与其原始位置完全相同的查找环境中被解析。结果是普通的BlockStmtUnparsedStmt不会存活到后续阶段。触发重新解析的调用点位于 checker 中在本文 watched paths 之外。IfStmt谓词与两分支IfStmt持有谓词与两个分支的原始Stmt*。没有else的if其negativeStatement槽位为 null。afterLoc字段是解析器消费完两个分支后正在查看的位置Parser::parseIfStatement末尾的tokenReader.peekLoc()slang-parser.cpp即整个if之后的第一个 token。if let没有专属节点。当ParseStatement在if后两个 token 处看到let时调用Parser::parseIfLetStatement在解析期把该形式脱糖成一个SeqStmt其中包含一个DeclStmt绑定合成的$OptVar后随一个普通IfStmt另一个合成的LetDecl绑定到$OptVar.value被前置到正分支。因此追踪if let的读者应在 AST 中找SeqStmt而不是某个独立的类。循环家族Loop familyForStmt、WhileStmt、DoWhileStmt都派生自LoopStmt后者派生自BreakableStmt再往上派生自ScopeStmt——因此每个循环都可break但只有for拥有自己的ScopeDecl。ForStmt::statement是循环体initialStatement按语句而非表达式解析以便DeclStmt能引入循环变量Parser::ParseForStatementslang-parser.cpp会诊断该位置任何既非DeclStmt也非ExpressionStmt的内容同时保留已构建的循环节点以便解析恢复。写在该位置的块——for ({ int i 0; } n 3; n n 1)——正是触发它的形态用户看到的是E20001unexpected statement, expected expression报告在该违规语句上。这是 gap-intake 修复81ec1b4a473c的结论证据在 slang-parser.cppParseStatement解析初始语句任何非DeclStmt/ExpressionStmt的内容触发Diagnostics::UnexpectedTokenExpectedTokenType其actualToken statement、expectedToken expression对应错误码定义于 slang-diagnostics.luaid 20001unexpected ~actualToken, expected ~expectedToken。bundle 测试 forstmt-init-non-expression-rejected.slang 正是用for ({ int i 0; } n 3; n n 1)固化 unexpected statement, expected expression 输出。UnscopedForStmt是 HLSL 兼容形态同一函数在getSourceLanguage()为SourceLanguage::HLSL时创建它而非ForStmt。这里有一个容易踩坑的重要事实输入语言由输入文件的扩展名决定不能用-lang选择。在OptionsParser::addInputPathslang-options.cpp中扩展名测试先于语言值生效以.slang结尾的路径直接进入addInputSlangPath因此命令行上的-lang hlsl只设置CompilerOptionName::Language永远不会到达它语言值只用于扩展名未决定的情况否则findSourceLanguageFromPath也会从扩展名推导。所以同一份源码编译为foo.hlsl时循环变量泄漏编译为foo.slang时则不会且给.slang编译加-lang hlsl也不会改变这一点。在 HLSL 情形下解析器会填充scopeDecl但从不压栈该作用域于是循环变量泄漏进外围作用域。关于用户如何选择 HLSL 输入这一缺口gap9f664f7cf462被暂缓deferredwatched_paths中没有决定输入源语言的内容——slang-parser.cpp 只读取getSourceLanguage()而Parser是从其调用方slang-parser.h同样未受监视收到该值的。真正回答该问题的扩展名表位于 slang-options.cpp{.hlsl, SLANG_SOURCE_LANGUAGE_HLSL, SLANG_STAGE_NONE}不在watched_paths内且 bundle 中没有编译 HLSL 输入的测试。若把watched_paths扩展到source/slang/slang-options.cpp即可闭环此缺口。do语句由Parser::ParseDoStatementslang-parser.cpp解析它先解析函数体、再决定构建什么后续有while则产生DoWhileStmt后续有catch则产生CatchStmt其余情况报错。ReturnStmtreturn既是控制流终结符也是函数结果的载体ReturnStmt::expression是被返回的Expr*裸returnvoid 函数中则为 null。checker 会把该表达式与外围函数声明的返回类型比对。CompileTimeForStmt编译期展开的循环基于范围的循环其边界必须是编译期常量rangeBeginVal与rangeEndVal是由检查阶段填充的IntVal*。解析器从$for (name in Range(...))语法产生它parseCompileTimeStmtslang-parser.cpp消费$parseCompileTimeForStmtslang-parser.cpp消费其余部分。Range在该位置是必需的、字面意义上的关键字解析器用ReadToken(Range)读取它slang-parser.cpp其失败路径是Unexpected(parser, expected)slang-parser.cpp→Diagnostics::UnexpectedTokenExpectedTokenName即E20004unexpected identifier, expected Range定义于 slang-diagnostics.luaid 20004消息 unexpected ~actualToken, expected ~expectedTokenName。这是 gap-intake 修复acf54503f940的结论。两种拼写及其迭代值均半开区间单参形式$for (i in Range(4))rangeBeginExpr保持 null迭代 0、1、2、3共 4 次。bundle 测试 compiletimefor-unrolls.slang 用Range(4)求和 0123 固化该行为。双参形式$for (i in Range(2, 5))逗号把第一个参数移入rangeBeginExpr迭代 2、3、4。测试 compiletimefor-two-argument-range.slang 固化之。其他标识符出现在该位置则报E20004由测试 compiletimefor-range-keyword-required.slangunexpected identifier, expected Range固化。循环变量是解析器创建的VarDecl被加入语句自身的ScopeDecl因此函数体可以引用它。TargetSwitchStmt、StageSwitchStmt、TargetCaseStmtTargetSwitchStmt是编译期按 capability 匹配的静态分发每个TargetCaseStmt携带一个 capability 代码和一个函数体。StageSwitchStmt形状相同但按管线阶段pipeline stage分发两者共享同一解析器parseTargetSwitchStmt与parseStageSwitchStmtslang-parser.cpp仅在调用parseTargetSwitchStmtImpl前分配哪个节点上不同。该共享实现有两个从表格中看不到的细节capability 名字在解析期就被解析findCapabilityName把 token 映射为CapabilityName存入TargetCaseStmt::capabilityint32_t未识别的名字立即被诊断为Diagnostics::UnknownTargetName——E29110unknown target name slang-diagnostics.luaid 29110消息 unknown target name ~name。该 case 仍会被记录但capability留在CapabilityName::Invalid于是语义检查在同一标签上再报第二个错误E36109Invalid cannot be used as a target_switch case.slang-diagnostics.luaid 36109消息 ~capability cannot be used as a target_switch case.。这是 gap-intake 修复8d82d193316d的结论证据在 slang-parser.cpp诊断UnknownTargetName后仍存储capability (int32_t)CapabilityName::Invalid而该Invalid存储值正是 checker 在 slang-check-stmt.cpp在本文 watched paths 之外添加Diagnostics::InvalidTargetSwitchCase的原因。bundle 测试 targetcase-unknown-name-rejected.slang 在同一标签上固化了两条消息。default:标签被记录为capabilityToken内容已清空的TargetCaseStmt。堆叠标签case a: case b: ...每个标签产生一个TargetCaseStmt全部指向同一个函数体Stmt因此遍历targetCases的消费者会多次看到该函数体。DeferStmt何时真正执行由 IR 降级决定Parser::ParseDeferStatementslang-parser.cpp读取defer关键字后跟一条Stmt——因此延迟块不需要尾随分号——并存入DeferStmt::statement。节点不携带其他状态延迟语句真正运行的时机由 IR 降级IR lowering决定而非 AST。可观察的规则gap-intake 修复20449a33ee45的结论是延迟语句在封闭作用域退出时运行——即包含该defer的块结束时也包括函数通过return提前离开该作用域的情形。注意break与throw虽出现在建议措辞中但没有任何 bundle 测试固化它们因此文档有意没有写入。相关 bundle 测试deferstmt-scope-exit-order.slang固化 body → deferred → after-block 的顺序deferstmt-at-function-entry.slang 与 deferstmt-runs-on-early-return.slang固化提前return时延迟打印落在被调用方最后一条打印与调用方下一条打印之间。降级的完整说明参见 04-ast-to-ir.md。ThrowStmt 与 CatchStmt可错误函数模型的两半CatchStmt同时持有被保护体tryBody与处理器handleBody错误参数是ParamDecl使 catch 处理器拥有完整类型的局部变量。errorVar为 null 表示不绑定错误值的 catch-all。注意语句级拼写是do { ... } catch (e) { ... }——没有try语句语句位置的try会被路由到Parser::ParseExpressionStatement因为try是表达式关键字。一个do后面可以跟多个catch子句Parser::ParseDoCatchStatementslang-parser.cpp会将其链接起来当下一个 token 是catch时循环继续把刚构建的CatchStmt作为下一个的tryBody于是n个 catch 子句变成n层嵌套的CatchStmt最外层被返回。DeclStmt、ExpressionStmt 与 EmptyStmt样板包装器这些样板包装器的存在是为了让语句文法保持统一。DeclStmt允许任意DeclBase出现在语句位置典型用途是局部变量声明ExpressionStmt允许任意Expr以副作用为目的使用。checker 分别校验每种类型。EmptyStmt是解析器为裸;产出的节点因此空的语句槽位例如循环体只有;仍有具体的Stmt。前两者的选择需要回溯backtracking对以标识符或::开头的语句ParseStatement先试探性解析一个类型若随后是标识符则回卷 token 读取器通过Parser::parseVarDeclrStatementslang-parser.cpp把整条重新解析为声明否则回卷并调用ParseExpressionStatement。if之后紧跟;是可疑而非非法该情形仍产出EmptyStmt但同时报告Diagnostics::UnintendedEmptyStatement——警告E20101potentially unintended empty statement at this location; use {} instead.。这是 gap-intake 修复df583f6f8952的结论证据在 slang-parser.cpp——只有当父节点是IfStmt时才抛出该诊断随后仍构建EmptyStmt条目以warning(...)声明slang-diagnostics.luaid 20101bundle 测试 emptystmt-after-if-diagnosed.slang 固化了该打印文本。因为是警告而非错误程序仍会继续编译。LabelStmt、BreakStmt、ContinueStmt 与 DiscardStmtSlang 支持带标签语句与带标签 break。LabelStmt把标签 token 附着到内部语句上ParseStatement看到标识符紧跟:时选择Parser::parseLabelStatementslang-parser.cppBreakStmt::targetLabel可选地命名要跳出的封闭带标签循环或switch。targetLabel到BreakableStmt::uniqueID的解析由语义检查完成。ContinueStmt是重启最近封闭循环的兄弟JumpStmt且不接受标签。DiscardStmt是仅限片元着色器的控制流语句discard丢弃当前像素不携带操作数。RequireCapabilityStmt语句级 capability 断言断言外围函数要求列出的 capability 原子。Parser::ParseRequireCapabilityStatementslang-parser.cpp识别__requireCapability关键字并在读取每个名字时通过findCapabilityName校验无法解析的名字被丢弃并诊断为Diagnostics::UnknownCapability——E36105unknown capability name .slang-diagnostics.luaid 36105消息 unknown capability name ~capability.。函数体内可接受的拼写是__requireCapability(hlsl);。gap-intake 修复98d151850f5e给出的细节名字在while (true)循环中读取遇到逗号时AdvanceIf(this, TokenType::Comma)slang-parser.cpp继续因此一条语句可以列出多个原子语句以ReadToken(TokenType::RParent)/ReadToken(TokenType::Semicolon)slang-parser.cpp收尾即)之后必须跟;。只有被接受的 token 才以原始Token存入requiredCaps交由 targets.md 所述 capability 系统解释。相关测试 requirecapability-in-function.slang 在函数体内编译__requireCapability(hlsl);requirecapability-unknown-name-rejected.slang 固化该消息。诊断错误码速查本文连同 gap-intake 报告共涉及六个用户可见的诊断码全部定义于 source/slang/slang-diagnostics.lua可在 slang-diagnostics-helpers.lua 的 E20001–E20012 注释范围内对照错误码类别消息触发位置E20001errorunexpected statement, expected expressionfor头部的初始位置出现非声明/非表达式语句E20004errorunexpected identifier, expected Range$for的Range位置出现其他标识符E20101warningpotentially unintended empty statement at this location; use {} instead.if后紧跟裸;E29110errorunknown target name __target_switch中出现未识别的 capability 名E36105errorunknown capability name .__requireCapability中出现无法解析的名字E36109error cannot be used as a target_switch case.同一标签在E29110之后由 checker 追加需要说明的边界gap-intake 报告指出诊断 id、严重级别与消息文本都位于 source/slang/slang-diagnostics.lua但它不在本文页面的watched_paths中因此这五个新增代码E20001、E20004、E20101、E29110、E36105、E36109目前是未被追踪的将其加入watched_paths可以闭环验证04-ast-to-ir.md 已在相同条件下引用这些代码。此外本文指向的 04-ast-to-ir.md 完全未提及DeferStmt该指针目前是悬空的值得在该页开一个缺口。扩展阅读base.md —Stmt、ModifiableSyntaxNode基类。declarations.md —DeclStmt包装任意DeclBase。expressions.md — 语句中的每个Expr*槽位。02-parse-ast.md — 两阶段函数体解析。scopes.md —ScopeStmt持有的ScopeDecl如何参与查找。04-ast-to-ir.md —DeferStmt、ThrowStmt、CompileTimeForStmt、TargetSwitchStmt的 IR 降级。grammar.md#statements — 语句文法产生式。验证用 bundle 测试目录docs/generated/tests/design/ast-reference/statements/。【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表