Gmsh 源码深度解析-第 3 章 脚本语言与解析器

Gmsh 源码深度解析-第 3 章 脚本语言与解析器

本章目标:讲清 Gmsh 自研脚本语言(GEO 语言)的完整实现------词法分析、语法分析、表达式求值、 变量与结构体系统、控制流、宏机制,以及这些命令如何落到几何内核。同时说明 Python/Julia 等 外部语言与解析器的关系。

第 2 章讲了"几何内核能做什么";本章讲"用户如何让内核去做"------这门 DSL 是 Gmsh 最被低估的 子系统之一。

3.1 脚本语言全景

3.1.1 GEO 语言:为几何而生的小型 DSL

Gmsh 的"第一语言"是 GEO 脚本语言 ,.geo 文件长这样(取自 tutorials/t1.geo):

geo 复制代码
lc = 1e-2;
Point(1) = {0, 0, 0, lc};
Point(2) = {.1, 0, 0, lc};
Point(3) = {.1, .3, 0, lc};
Point(4) = {0, .3, 0, lc};
Line(1) = {1, 2};
Line(2) = {3, 2};
Line(3) = {3, 4};
Line(4) = {4, 1};
Curve Loop(1) = {4, 1, -2, 3};
Plane Surface(1) = {1};

它不是通用编程语言,而是为几何建模量身定制的领域语言(DSL) :语法层级(Point → Line → Curve Loop → Surface → Volume)与几何拓扑一级一级对应。第 2 章已经看到,自研内核 GEO_Internals 的容器与此层级一一对应------这绝非巧合:语言结构直接映射内核数据结构,是这门语言最核心的 设计。

3.1.2 多元语言策略

除 GEO 外,Gmsh 还支持:

  • Python / Julia :通过公开 API(gmsh.py / gmsh.jl,第 10 章)编写等价脚本。.py 文件由 Python 解释器执行并回调 libgmsh,不经过下面的解析器。
  • 外部脚本/求解器 :GUI 与 Onelab 可启动外部程序(OnelabRun),通过 socket 传网格/参数 (第 10 章)。
  • JavaScript:WASM 构建的 JS API。

因此要区分两条路径:

text 复制代码
.geo 文件 ──> Gmsh.l/Gmsh.y 解析器 ──> GEO_Internals/OCC_Internals ──> GEntity
.py/.jl 文件 ──> 各自解释器 ──> public API (gmsh::model::geo::*) ──> 同一批底层实现

本章只讲第一条路径(解析器),因为它是 Gmsh 独有的、最复杂的部分。

3.1.3 内核分派:gmsh_yyfactory

src/parser/Parser.h(第 444 行)导出一个全局变量:

cpp 复制代码
extern std::string gmsh_yyfactory;   // 当前几何内核名:"Built-in" 或 "OpenCASCADE"

它是理解"解析器与内核关系"的钥匙:每条几何命令在执行时都根据 gmsh_yyfactory 选择调用 GEO_Internals 还是 OCC_Internals 。SetFactory("OpenCASCADE") 命令把 gmsh_yyfactory 置为 "OpenCASCADE",之后所有几何命令都走 OCC。第 3.10 节会看到 Extrude 规则里这个分派的具体写法。

3.2 flex + bison 管线

3.2.1 生成流程

Gmsh 的解析器用经典工具链构建:

text 复制代码
Gmsh.l  ──(flex)──>  Gmsh.yy.cpp    词法分析器(产生 token,返回 tXXX)
Gmsh.y  ──(bison)──> Gmsh.tab.cpp   语法分析器(LALR(1),含语义动作)
                     Gmsh.tab.hpp   token 编号与 %union 定义

两个生成物(Gmsh.yy.cpp、Gmsh.tab.cpp)都提交进仓库 。好处是:普通用户即使没有安装 flex/bison 也能编译 Gmsh ------这是开源项目降低构建门槛的常见做法;只有修改 .l/.y 的开发者 才需要重新生成。

3.2.2 对外接口

src/parser/Parser.h(第 14--16 行)只暴露三个函数:

cpp 复制代码
int  gmsh_yyparse();    // 启动解析(yacc/bison 生成的入口)
int  gmsh_yylex();      // 取下一个 token(flex 生成的入口)
void gmsh_yyflush();    // 清空输入缓冲

其余都是全局解析器状态(第 435--445 行):

cpp 复制代码
extern FILE *gmsh_yyin;
extern int gmsh_yylineno;
extern char *gmsh_yytext;
extern int gmsh_yyviewindex;
extern std::string gmsh_yyname;
extern int gmsh_yyerrorstate;
extern std::map<std::string, gmsh_yysymbol> gmsh_yysymbols;
extern std::map<std::string, std::vector<std::string>> gmsh_yystringsymbols;
extern std::string gmsh_yyfactory;
extern NameSpaces gmsh_yynamespaces;

这组全局变量是解析器与外界共享的全部状态 :输入文件、行号、当前文本、变量表、结构体 命名空间、当前内核。注意到处都是全局变量 ------因为 flex/bison 生成的 C 代码天然依赖全局 状态(yyin/yytext/yylineno)。这是选择这套工具链的固有代价。

3.3 词法分析器 Gmsh.l

src/parser/Gmsh.l 定义了词法规则。逐块看。

3.3.1 自定义输入:逐行读取

YY_INPUT 宏(第 24--37 行)覆盖了 flex 的默认输入:

c 复制代码
#define YY_INPUT(buf,result,max_size)                       \
  {                                                         \
    int c = '*', n;                                         \
    for(n = 0; n < (int) max_size && (c = fgetc(yyin)) != EOF && c != '\n' && c != '\r'; ++n) \
      buf[n] = (char) c;                                    \
    if(c == '\n' || c == '\r') {                            \
      buf[n++] = (char) c;                                  \
      if(c == '\n') yylineno++;                             \
    }                                                       \
    if(c == EOF && ferror(yyin))                            \
      Msg::Error("Input in flex scanner failed");           \
    result = n;                                             \
  }

为什么逐行读? 因为 Gmsh 的 For 循环需要保存/回退输入位置 (第 3.8 节):逐行读取时, "记住文件位置"只需一个 fpos_t,"回到该行重读"也简单。行号 yylineno 在读到换行时自增, 用于报错定位。

3.3.2 注释、空白与字符串

text 复制代码
[\ \t\n\r\f]        /* 空白,忽略 */;
";"                 return tEND;                 // 分号 = 语句结束
"/*"                skipcomments();              // C 风格块注释
"//"                skipline();                  // 行注释
"\""                { parsestring('\"'); return tBIGSTR; }   // 双引号字符串
"\'"                { parsestring('\''); return tBIGSTR; }    // 单引号字符串

skipcomments()/skipline()/parsestring() 是手写辅助函数(在 .l 的 %{...%} 里声明), 因为 flex 的规则无法直接表达"读到 */ 为止""读到行尾为止""处理转义引号"。

3.3.3 自动编号:new* 系列------最精妙的一处

text 复制代码
"newreg"       { gmsh_yylval.d = NEWREG();       return tDOUBLE; }
"newp"         { gmsh_yylval.d = NEWPOINT();     return tDOUBLE; }
"newl"/"newc"  { gmsh_yylval.d = NEWCURVE();     return tDOUBLE; }
"newll"/"newcl"{ gmsh_yylval.d = NEWCURVELOOP(); return tDOUBLE; }
"news"         { gmsh_yylval.d = NEWSURFACE();   return tDOUBLE; }
"newsl"        { gmsh_yylval.d = NEWSURFACELOOP(); return tDOUBLE; }
"newv"         { gmsh_yylval.d = NEWVOLUME();    return tDOUBLE; }
"newf"         { gmsh_yylval.d = NEWFIELD();     return tDOUBLE; }

这是整个词法分析器最巧妙的设计 :newp/newc/... 不做词法层面的特殊处理,而是在扫描时 立即向内核索取一个新 tag (NEWPOINT() 等宏,定义在 Geo.h),然后把它当作浮点常量 (tDOUBLE)返回给语法分析器。

于是脚本里:

geo 复制代码
p1 = newp; Point(p1) = {x, y, z, lcar};

在解析器看来就是"把一个数值赋给 p1,再用 p1 建点"------自动编号被无缝融入了表达式系统, 无需任何特殊语法规则。这是一种"把语言特性下沉到词法层"的优雅做法。

t5.geo 的注释解释了两套编号策略:

If Geometry.OldNewReg is set to 0, the new tags are chosen as the highest current tag for each category ... plus one. By default, for backward compatibility, Geometry.OldNewReg is set to 1, and only two categories are used: one for points and one for the rest.

即默认(OldNewReg=1)只用两个计数器(点一个、其余一个),newp 与 newc 可能返回相同数字 ------无害,因为 tag 只在维度内唯一。设为 0 则每类独立。

3.3.4 运算符

第 75--92 行定义了完整的运算符集合:

text 复制代码
"="   → tAFFECT        "+=" → tAFFECTPLUS    "-=" → tAFFECTMINUS
"*="  → tAFFECTTIMES   "/=" → tAFFECTDIVIDE
":"   → tDOTS          "..."→ tDOTS          "::" → tSCOPE
"||"  → tOR            "&&" → tAND           "++" → tPLUSPLUS   "--" → tMINUSMINUS
"=="  → tEQUAL         "!=" → tNOTEQUAL      "<=" → tLESSOREQUAL ">=" → tGREATEROREQUAL
">>"  → tGREATERGREATER "<< "→ tLESSLESS

注意 : 与 ... 都映射到 tDOTS ------它们被 For 循环复用:For i In {1:10} 与 For i In {1...10} 是同一件事的不同写法。

3.3.5 关键字与别名表

第 94 行之后是庞大的关键字表,把标识符映射到 token。这张表里藏着 Gmsh 的"方言兼容层": 同一语义可有多种拼写。典型别名:

text 复制代码
ArcCos / Acos            → tAcos
ArcSin / Asin            → tAsin
ArcTan / Atan            → tAtan
ArcTan2 / Atan2          → tAtan2
Block / Box              → tBox
BooleanFuse / BooleanUnion → tBooleanUnion
BooleanCommon            → tBooleanIntersection
BooleanCut               → tBooleanDifference
CatmullRom               → tSpline
GetValue                 → tGetValue
GetStringValue           → tGetStringValue
TestLevel                → tTestLevel

BooleanFuse/BooleanCommon/BooleanCut 是给熟悉 OpenCASCADE 术语的用户准备的; CatmullRom 是样条的别名。"一词多拼在词法层一次映射到统一 token,语法层完全不必感知" ------这是把兼容性成本集中到一处的做法。

3.3.6 数字与字符串的识别

除关键字表外,Gmsh.l 还用正则识别数字(整数、浮点、科学计数法)与标识符:

text 复制代码
alpha   [a-zA-Z\_]
dieze   [\#]
special [\.]
digit   [0-9]
exp     [Ee][-+]?{digit}+
string  {alpha}({alpha}|{digit})*

这些模式定义的"标识符""数字"覆盖了脚本里所有字面量。

3.4 语法分析器 Gmsh.y

src/parser/Gmsh.y 是语法定义,是全书最大的文件之一。逐块看。

3.4.1 语义值联合体

第 170--179 行的 %union 定义不同 token 的语义值类型:

c 复制代码
%union {
  char *c;         // 字符串
  int i;           // 整数(维度、方向等)
  unsigned int u;  // 颜色
  double d;        // 数
  double v[5];     // 向量(最多 5 元:如 {x,y,z} 或 {axis,point,angle})
  Shape s;         // 一个几何形状((维度, 标签) 对)
  List_T *l;       // 形状列表或数值列表
  struct TwoChar c2;
}

两个最重要的类型:

  • Shape s :一个 (dim, tag) 对(第 2 章定义的寻址约定)。
  • List_T *l:形状/数值的列表。

几乎所有几何命令都在"shape 列表"上操作。double v[5] 承载向量参数(如 {dx,dy,dz} 或 旋转的 {x,y,z,ax,ay,az,angle} 需要更多分量,故留到 5)。

3.4.2 token 声明的信息量

%token 区域(第 181--226 行)本身就是一份功能清单。按主题分组:

数学函数:

text 复制代码
tExp tLog tLog10 tSqrt tSin tAsin tCos tAcos tTan tRand tStep
tAtan tAtan2 tSinh tCosh tTanh tFabs tAbs tFloor tCeil tRound tMin tMax
tFmod tModulo tHypot

列表/字符串函数:

text 复制代码
tList tLinSpace tLogSpace tListFromFile tCatenary
tPrintf tError tWarning tStr tSprintf tStrCat tStrPrefix tStrRelative tStrReplace
tAbsolutePath tDirName tStrSub tStrLen
tFind tStrFind tStrCmp tStrChoice tUpperCase tLowerCase tLowerCaseIn

几何图元:

text 复制代码
tPoint tCircle tEllipse tCurve tSphere tPolarSphere tSurface tSpline tVolume
tBox tCylinder tCone tTorus tEllipsoid tQuadric tShapeFromFile
tRectangle tDisk tWire tGeoEntity tNormal tCurvature

几何关系与属性:

text 复制代码
tPlane tRuled tTransfinite tPhysical tCompound tPeriodic tParent tUsing
tDegenerated tRecursive tSewing tEmbedded? tCharacteristic tLength
tParametric tElliptic

变换与布尔:

text 复制代码
tRotate tTranslate tSymmetry tDilate tExtrude tAffine tClosest
tBooleanUnion tBooleanIntersection tBooleanDifference tBooleanSection
tBooleanFragments tThickSolid tFillet tChamfer

网格命令:

text 复制代码
tRecombine tSmoother tSplit tDelete tCoherence tHealShapes
tIntersect tMeshAlgorithm tReverseMesh tMeshSize tMeshSizeFromBoundary
tRefineMesh tRecombineMesh tAdaptMesh tTransformMesh tRelocateMesh tReorientMesh
tSetFactory tThruSections tWedge
tLayers tScaleLast tHole tAlias tAliasWithOptions tCopyOptions
tQuadTriAddVerts tQuadTriNoNewVerts

控制与元编程:

text 复制代码
tFor tIn tEndFor tIf tElseIf tElse tEndIf tExit tAbort
tField tReturn tCall tSlide tMacro tShow tHide
tGetValue tGetStringValue tGetEnv tGetString tGetNumber
tUnique tSetMaxTag

变量与结构体:

text 复制代码
tDefineNumber tDefineStruct tNameStruct tDimNameSpace tAppend
tDefineString tSetNumber tSetTag tSetString
tDefineConstant tUndefineConstant

拓扑与文件:

text 复制代码
tHomology tCohomology tBetti tExists tFileExists tGetForced tGetForcedStr
tCreateTopology tCreateGeometry tClassifySurfaces
tRenumberMeshNodes tRenumberMeshElements tCreateMeshEdges tCreateMeshFaces
tDistanceFunction

系统信息与 Onelab:

text 复制代码
tCpu tMemory tTotalMemory tToday tCpu tCurrentDirectory tCurrentFileName
tOnelabAction tOnelabRun tCodeName tMass tCenterOfMass tMatrixOfInertia
tGMSH_MAJOR_VERSION tGMSH_MINOR_VERSION tGMSH_PATCH_VERSION
tGmshExecutableName tSetPartition
tNameToString tStringToName tUnsplitWindow tSyncModel tNewModel
tBoundingBox tDraw tSetChanged tFixRelativePath

后处理/视图:

text 复制代码
tText2D tText3D tInterpolationScheme tTime tCombine
tColor tColorTable
tView? tPlugin

这份清单说明 GEO 语言远不止几何------它还有字符串处理、列表操作、文件查询、系统信息、Onelab 交互、拓扑计算、后处理视图创建。它是一门"小而全"的 DSL。

3.4.3 类型化非终结符

%type 区域把非终结符按语义值类型分组(第 228--245 行起):

c 复制代码
%type <d> FExpr FExpr_Single DefineStruct NameStruct_Arg GetForced_Default
%type <d> LoopOptions
%type <v> VExpr VExpr_Single CircleOptions TransfiniteType
%type <i> NumericAffectation NumericIncrement BooleanOperator BooleanOption
%type <i> PhysicalId_per_dim_entity GeoEntity GeoEntity123 GeoEntity12 GeoEntity012
%type <i> TransfiniteArrangement RecombineAngle
%type <i> Append AppendOrNot
%type <u> ColorExpr
%type <c> StringExpr StringExprVar SendToFile HomologyCommand
%type <c> LP RP tSTRING_Reserved GetForcedStr_Default
%type <c> StringIndex String__Index
%type <l> MultiStringExprVar SurfaceConstraints
%type <l> RecursiveListOfStringExprVar Str_BracedRecursiveListOfStringExprVar

分组规律:

  • FExpr(浮点表达式)→ <d>;
  • VExpr(向量)→ <v>;
  • StringExpr(字符串表达式)→ <c>;
  • 列表类 → <l>;
  • 维度/整数 → <i>;
  • 颜色 → <u>。

这是 bison 类型检查的基础------不同类别用不同语义值类型,能在解析期避免"把字符串当数值"。

3.4.4 "维度约束下沉到文法"

第 2 章提到 Gmsh 用 (维度, tag) 寻址。语法层通过一组带维度约束的非终结符 在解析期就 挡住越界写法。以 GeoEntity(第 2307 行起)为例:

c 复制代码
GeoEntity :
    tPoint   { $$ = 0; }
  | tCurve   { $$ = 1; }
  | tSurface { $$ = 2; }
  | tVolume  { $$ = 3; }
  | tGeoEntity '{' FExpr '}' {
      $$ = (int)$3;
      if($$ < 0 || $$ > 3) yymsg(0, "GeoEntity dim out of range [0,3]");
    }
;

它派生三类:

  • GeoEntity123(只允许 1--3 维,第 2323 行);
  • GeoEntity12(1--2 维,第 2337 行);
  • GeoEntity012(0--2 维,第 2349 行)。

于是 Physical Point{...}、Curve Loop、Surface Loop 等命令能在文法层面限定"这里只能出现 点/曲线/面"。Gmsh 把一部分类型检查下沉到了文法里------少见但很有效的设计。

3.5 表达式与数学函数

3.5.1 两套表达式体系

  • FExpr(浮点表达式):算术、比较、逻辑、位运算、函数调用;
  • VExpr(向量表达式):逗号分隔的浮点序列,用于坐标/轴/角度向量。

数学函数在语法动作里直接调用 C 标准库或 Gmsh 的 Numeric 工具。示例脚本:

geo 复制代码
r = 2.0;
a = Sin(30*Pi/180);          // Pi 是保留常量
b = Max(1, Min(a, 10));      // 函数可嵌套
c = Fmod(17, 5);
d = Random();                // 随机数

Pi 是词法层的保留 token(tPi,第 184 行)。MPI_Rank/MPI_Size(token tMPI_Rank/ tMPI_Size)让并行脚本按 rank 分支。Rand()/Random() 依赖第 4 章 GenerateMesh 起点的 固定随机种子 srand(CTX::instance()->mesh.randomSeed),以保证网格可复现。

3.5.2 两套求值器

  • 解析器内的求值 :脚本里的表达式在语法动作里编译期绑定 (写死在生成的 Gmsh.tab.cpp)。
  • mathEvaluator :运行期对字符串表达式求值(src/numeric/mathEvaluator.h)。

分工是:脚本里的表达式用解析器 ;运行期动态字符串(尺寸场的 MathEval 字段、插件参数) 用 mathEvaluator。后者的变量表可以随配置变化,因此不能在编译期绑定。

3.5.3 字符串与列表函数

tStr/tSprintf/tStrCat/tStrPrefix/tStrRelative/tStrReplace/tStrSub/tStrLen/ tFind/tStrFind/tStrCmp/tStrChoice/tUpperCase/tLowerCase 提供完整的字符串处理; tList/tLinSpace/tLogSpace/tListFromFile 提供列表生成(等差/等比/从文件读)。 这使 GEO 语言足以写"数据驱动"的脚本(如从文件读一组参数批量建模)。

3.6 变量系统

3.6.1 数值符号与字符串符号

解析器维护两组全局符号表(src/parser/Gmsh.y 第 71--72 行):

cpp 复制代码
std::map<std::string, gmsh_yysymbol> gmsh_yysymbols;                    // 数值变量
std::map<std::string, std::vector<std::string>> gmsh_yystringsymbols;   // 字符串变量

gmsh_yysymbol(src/parser/Parser.h 第 18--23 行)极简:

cpp 复制代码
class gmsh_yysymbol {
public:
  bool list;
  std::vector<double> value;
  gmsh_yysymbol() : list(false) {}
};

list 标志区分"标量"与"列表":

  • a = 1; → list=false、value={1};
  • a[] = {1,2,3}; → list=true。

value 统一用 std::vector<double> 存储,标量就是长度为 1 的向量。字符串变量则直接用 std::vector<std::string>。

3.6.2 赋值与复合赋值

脚本支持复合赋值与自增自减(词法层的 += -= *= /= ++ --):

geo 复制代码
i = 0;
i += 1;
i++;              // 递增

赋值语义由 assignVariable/assignVariables/incrementVariable(Gmsh.y 第 113--116 行声明) 实现,它们处理:

  • 单值 vs 列表 :assignVariable(name, index, assignType, value) 的 index 参数支持 a[3] = ... 这类索引赋值;
  • 类型转换:数值↔字符串;
  • 复合赋值 :+=/-=/... 由 assignType 区分。

3.6.3 变量相关命令

  • tSetNumber/tSetString/tSetTag:直接设变量/标签;
  • tDefineNumber/tDefineString:定义(可带默认与文档字符串);
  • tSetMaxTag :设置各类别的最大 tag(影响 new* 的编号起点);
  • tGetNumber/tGetString:取变量值;
  • tGetForced/tGetForcedStr :取变量值,若未定义则给默认 (对应规则 tGetForced '(' Struct_FullName GetForced_Default ')',第 5022 行)。

GetForced 的语法很有意思(第 5022--5034 行给了四种形式):支持 Struct_FullName、 .tSTRING_Reserved、带 [...] 索引的变体。这让"读一个可能未定义的参数"成为单条表达式。

3.7 结构体与命名空间

3.7.1 三级寻址

GEO 语言有轻量"结构体"系统,用于把多个参数打包(如网格属性)。数据结构在 src/parser/Parser.h:

cpp 复制代码
class Struct {                          // 一个具名成员集合
public:
  Struct(int tag, std::map<std::string, std::vector<double>> &fopt,
         std::map<std::string, std::vector<std::string>> &copt, int member_ValMax);
  int append(int tag, ...);             // 追加成员
  int getTag() const;
  int getMember(std::string &key_member, double &out, int index) const;
  int getMember(std::string &key_member, const std::string *&out, int index) const;
  int getMember_Dim(std::string &key_member, int &out) const;
  int getMember_Vector(std::string &key_member, const std::vector<double> *&out_vector) const;
  int getMember_Vector(std::string &key_member, const std::vector<std::string> *&out_vector) const;
  int getMember_ValMax() const;
  void sprint(std::string &str, const std::string &struct_name, const std::string &struct_namespace) const;
private:
  int _tag, _member_ValMax;
  std::map<std::string, std::vector<double>> _fopt;              // 数值成员
  std::map<std::string, std::vector<std::string>> _copt;         // 字符串成员
};

typedef std::map<std::string, Struct> Map_string_Struct;

class Structs : public Map<std::string, Struct> {   // 命名空间下的结构体集合
public:
  int defStruct(std::string &struct_name, ...);     // 定义结构体(分配 tag)
  int get_key_struct_from_tag(int tag, const std::string *&key_struct) const;
  void sprint(std::vector<std::string> &strs_out, const std::string &struct_namespace) const;
private:
  int _max_tag;
};

class NameSpaces : public Map<std::string, Structs> {   // 命名空间 → 结构体 → 成员
public:
  int defStruct(std::string &key_namespace, std::string &key_name, ..., int &tag_out, ...);
  int getTag(std::string &key_namespace, std::string &key_name, double &out) const;
  int getMember(std::string &key_namespace, std::string &key_name, std::string &key_member, double &out, int index=0) const;
  ...
};

这是"命名空间 → 结构体 → 成员"的三级寻址 。Struct 同时支持数值成员(_fopt)与字符串 成员(_copt),成员的 std::vector 允许多值(如向量成员)。

defStruct 的 tag 分配逻辑(第 224--244 行):若成员里有 Tag 就强制用它的值,否则自动递增 (_max_tag)。这允许"手工指定结构体编号"。

3.7.2 脚本命令

geo 复制代码
DefineStruct MyStruct;
MyStruct.Field1 = 1.0;

相关命令 tDefineStruct/tNameStruct/tDimNameSpace/tAppend 在 Gmsh.y 里通过一组 treat_Struct_FullName_* 函数实现(第 144--162 行声明):

cpp 复制代码
double treat_Struct_FullName_Float(char* c1, char* c2, int type_var=1, int index=0,
                                    double val_default=0., int type_treat=0);
double treat_Struct_FullName_dot_tSTRING_Float(char* c1, char* c2, char* c3, int index=0, ...);
List_T *treat_Struct_FullName_dot_tSTRING_ListOfFloat(char* c1, char* c2, char* c3);
char* treat_Struct_FullName_String(char* c1, char* c2, int type_var=1, int index=0, ...);

命名规律说明支持三级访问:Struct_FullName(ns::name)、.dot.tSTRING(.member)、 ListOfFloat/ListOfString(列表成员)。这套机制让脚本可以把尺寸场/网格选项组织成结构体 一次性传递 ------例如第 4 章尺寸场的 Field[i].Options 就受益于这类访问。

3.8 控制流:For 与 If 的"回读"实现

GEO 语言的循环没有编译成跳转指令 ,而是用了一个非常朴素的技巧:保存输入位置,反复重读 同一段文本。

3.8.1 全局状态

核心状态(src/parser/Gmsh.y 第 86--91 行):

cpp 复制代码
static int statusImbricatedTests[MAX_RECUR_TESTS];
static int ImbricatedLoop = 0, ImbricatedTest = 0;
static fpos_t yyposImbricatedLoopsTab[MAX_RECUR_LOOPS];
static int yylinenoImbricatedLoopsTab[MAX_RECUR_LOOPS];
static double LoopControlVariablesTab[MAX_RECUR_LOOPS][3];   // [起点, 终点, 步长]
static std::string LoopControlVariablesNameTab[MAX_RECUR_LOOPS];
  • ImbricatedLoop/ImbricatedTest:当前嵌套的循环/分支深度;
  • yyposImbricatedLoopsTab[]:每层循环的输入文件位置 (fpos_t);
  • LoopControlVariablesTab[][3]:每层循环的起点/终点/步长;
  • statusImbricatedTests[]:每个 If 分支是否已执行的栈。

两个常量(第 64--65 行)限制嵌套深度:

c 复制代码
#define MAX_RECUR_TESTS 100
#define MAX_RECUR_LOOPS 100

3.8.2 For 的语义动作

第 3384--3417 行给出 For 的四种形式。最基本的一种:

c 复制代码
tFor '(' FExpr tDOTS FExpr ')'
  {
    LoopControlVariablesTab[ImbricatedLoop][0] = $3;   // 起点
    LoopControlVariablesTab[ImbricatedLoop][1] = $5;   // 终点
    LoopControlVariablesTab[ImbricatedLoop][2] = 1.0;  // 步长
    LoopControlVariablesNameTab[ImbricatedLoop] = "";
    fgetpos(gmsh_yyin, &yyposImbricatedLoopsTab[ImbricatedLoop]);   // ★ 记住位置
    yylinenoImbricatedLoopsTab[ImbricatedLoop] = gmsh_yylineno;
    if($3 > $5)
      skip("For", "EndFor");                            // 空区间直接跳过整段
    else
      ImbricatedLoop++;
    if(ImbricatedLoop > MAX_RECUR_LOOPS - 1) {
      yymsg(0, "Reached maximum number of imbricated loops");
      ImbricatedLoop = MAX_RECUR_LOOPS - 1;
    }
  }

带步长的形式(第 3401 行)多了一个 FExpr,其余相同。带循环变量的形式(第 3418 行)把变量名 记进 LoopControlVariablesNameTab,并注册到 gmsh_yysymbols。

工作机制 :解析器遇到 EndFor 时,把循环变量自增;若还没到终点,就 fsetpos(gmsh_yyin, &yyposImbricatedLoopsTab[...]) ------跳回 For 之后的位置重新解析循环体 , 直到变量越过终点才继续向后。这就是为什么循环体每轮被"重新执行一遍"------它的副作用(建点、 建面、newp 取新编号)自然就累加了。

3.8.3 skip 与 skipTest

skip(skip, until) 与 skipTest(...)(第 109--111 行声明)是"按文本标记跳过一段"的工具。 当 For 的区间为空(如 1:0)时,需要跳过整个循环体,skip("For","EndFor") 就从当前位置 一直扫描到 EndFor。它是无 AST 执行模型的必要补充。

3.8.4 If 系列

If/ElseIf/Else/EndIf 用 statusImbricatedTests[] 栈记录每个分支是否已经执行, 从而正确处理 ElseIf/Else。对应的规则在第 3533 行(tIf '(' FExpr ')')。

3.8.5 代价与收益

代价 :循环体被反复做词法/语法分析,有性能开销;循环体内不能有"跨轮次的状态推断"。 收益 :解析器实现大幅简化------无需构造 AST、无需执行引擎、无需变量作用域管理。对几何脚本 而言,循环体通常很短、迭代次数有限,这个代价可接受。这是"简单优先"的工程取舍。

3.9 宏与 FunctionManager

3.9.1 宏的语法

geo 复制代码
Macro CheeseHole
  p1 = newp; Point(p1) = {x, y, z, lcar3};
  ...
Return

相关规则:tMacro tSTRING ...(第 3497 行)与 tMacro StringExpr(第 3505 行)定义宏; Call String__Index tEND(第 3519 行)与 Call StringExpr tEND(第 3526 行)调用宏。Return 结束宏体。

3.9.2 FunctionManager:全局单例

宏由 src/parser/FunctionManager.h 管理:

cpp 复制代码
// Singleton, one function manager for all parsers.
class FunctionManager {
  FunctionManagerMap *functions;   // 函数名 → 定义(文件、行号)
  FunctionManagerStack *calls;     // 调用栈
  FunctionManager();
  static FunctionManager *instance;
public:
  static FunctionManager* Instance();
  int createFunction(const std::string &name, FILE *f, const std::string &filename, int lineno);
  int enterFunction(const std::string &name, FILE **f, std::string &filename, int &lineno) const;
  int leaveFunction(FILE **f, std::string &filename, int &lineno);
  void clear();
};

机制直观:

  1. Macro name ... Return 定义时,createFunction 把宏体在源文件里的位置(文件 + 起始行) 记入 functions;
  2. Call name; 时,enterFunction 把当前解析位置压入 calls 栈,并把 yyin 切到宏体;
  3. 遇到 Return 则 leaveFunction 弹栈并恢复。

FunctionManager 是单例 (static FunctionManager *instance),注释明说"one function manager for all parsers"------所有解析器实例共享同一套宏定义 ,从而允许跨文件复用宏。 t5.geo 用 Macro CheeseHole 在立方体上挖球形孔,就是典型用例。

单例设计也解释了为什么需要 clear():重置时清空宏表与调用栈。

3.9.3 宏与 For 的交互

宏体在被 Call 时是"跳到宏体文本处解析",而 For 是"跳回循环起点重读"。两者都用 fgetpos/fsetpos,因此可以嵌套 (宏里有循环、循环里有宏调用)。FunctionManagerStack 与 ImbricatedLoop 栈各自记录自己的位置,互不干扰。

3.10 几何命令如何落地:Extrude 全解

前面讲"语言怎么解析",这一节讲"命令怎么执行"。以最复杂的 Extrude 为例。

3.10.1 平移拉伸

第 3602--3619 行:

c 复制代码
tExtrude VExpr '{' ListOfShapes '}'
  {
    std::vector<std::pair<int, int>> inDimTags, outDimTags;
    ListOfShapes2VectorOfPairs($4, inDimTags);
    bool r = true;
    if(gmsh_yyfactory == "OpenCASCADE" && GModel::current()->getOCCInternals())
      r = GModel::current()->getOCCInternals()->extrude(inDimTags, $2[0], $2[1], $2[2], outDimTags);
    else
      r = GModel::current()->getGEOInternals()->extrude(inDimTags, $2[0], $2[1], $2[2], outDimTags);
    if(!r) yymsg(0, "Could not extrude shapes");
    $$ = $4;
    List_Reset($$);
    VectorOfPairs2ListOfShapes(outDimTags, $$);
  }

四个关键点:

  1. gmsh_yyfactory 分派双内核------第 2 章的双内核在这里真正"接上"脚本。
  2. 输入形状列表 → (dim,tag) 对 :ListOfShapes2VectorOfPairs($4, inDimTags)。
  3. 输出 (dim,tag) 对 → 形状列表 :VectorOfPairs2ListOfShapes(outDimTags, $$)。
  4. 输入形状列表被复用为输出 :$$ = $4; List_Reset($$); ------执行完后表达式的结果是新生成 的形状列表 ,所以脚本里可以写 out[] = Extrude{...}{...}; 继续在下游使用。

3.10.2 旋转与扭转

第 3620--3637 行是旋转:

c 复制代码
tExtrude '{' VExpr ',' VExpr ',' FExpr '}' '{' ListOfShapes '}'
  {
    // $3 = 轴向量, $5 = 轴上的点, $7 = 角度
    if(gmsh_yyfactory == "OpenCASCADE" && ...)
      r = ...->revolve(inDimTags, $5[0],$5[1],$5[2], $3[0],$3[1],$3[2], $7, outDimTags);
    else
      r = ...->revolve(...);
  }

第 3638--3655 行是扭转(twist),并且有一条关键的内核限制:

c 复制代码
tExtrude '{' VExpr ',' VExpr ',' VExpr ',' FExpr '}' '{' ListOfShapes '}'
  {
    if(gmsh_yyfactory == "OpenCASCADE" && GModel::current()->getOCCInternals())
      yymsg(0, "Twisting extrude not available with OpenCASCADE geometry kernel");
    else
      r = GModel::current()->getGEOInternals()->twist(inDimTags, $7[0],$7[1],$7[2], $3[0],$3[1],$3[2], $5[0],$5[1],$5[2], $9, outDimTags);
  }

这就是第 2.1.3 节说的"OCC 不支持 twist"在代码里的样子------语法动作里直接报错。

3.10.3 带参数(Layers/Recombine)的拉伸

第 3656--3679 行是最常用的形式------拉伸 + 网格分层:

c 复制代码
tExtrude VExpr '{' ListOfShapes
  {
    extr.mesh.ExtrudeMesh = extr.mesh.Recombine = false;   // ★ 先清零默认值
    extr.mesh.QuadToTri = NO_QUADTRI;
    extr.mesh.ScaleLast = false;
  }
                   ExtrudeParameters '}'                    // 可选参数块
  {
    std::vector<std::pair<int, int>> inDimTags, outDimTags;
    ListOfShapes2VectorOfPairs($4, inDimTags);
    bool r = true;
    if(gmsh_yyfactory == "OpenCASCADE" && ...)
      r = ...->extrude(inDimTags, $2[0],$2[1],$2[2], outDimTags, &extr);
    else
      r = ...->extrude(inDimTags, $2[0],$2[1],$2[2], outDimTags, &extr);
    ...
  }

注意那个**"空的中间动作"**:它在进入 ExtrudeParameters 之前先把 extr 清成默认值,再让参数 块覆盖。这是"可选参数块 + 默认值"的标准 bison 写法------默认值必须在解析参数之前设置。

ExtrudeParameters 是子规则,处理 Layers{...}/Recombine;/QuadTriAddVerts; 等(对应 ExtrudeParams::mesh 的字段,第 2.10 节)。extr 是一个静态全局变量 (Gmsh.y 第 84 行 static ExtrudeParams extr;)------因为 bison 的语义值联合体放不下这么大的结构体。

3.10.4 这对理解绑定层的启示

Extrude 规则浓缩了四条设计原则:

  1. 双内核分派 (gmsh_yyfactory);
  2. 可选中缀 (ExtrudeParameters,默认值先清零);
  3. 同一 token 的不同规则(平移/旋转/扭转,靠参数个数区分);
  4. 输入列表复用为输出 ($$ = $4; List_Reset($$); VectorOfPairs2ListOfShapes(...))。

几乎所有几何命令都遵循这四条 ------理解 Extrude,就理解了 Gmsh 语法动作的一半。

3.11 其他关键命令的落地

3.11.1 Physical(物理组)

第 2629 行:

c 复制代码
ListOfShapes tPhysical GeoEntity '{' RecursiveListOfDouble '}'
  {
    // 把形状加入物理组、赋予编号
  }

变体包括 tPhysical ... '{' ListOfPhysicalNames '}'(第 2649 行,带名字)与 {...} 里用 ... (第 2708 行)。物理组在语义动作里写入 GModel::current()->setPhysicalName(...) 与内核的 PhysicalGroups 容器(第 2.14.2 节的 CreatePhysicalGroup)。

3.11.2 布尔

tBooleanUnion 等 token 的规则调用 OCC_Internals::booleanUnion(...)(第 2.8 节),并处理 {...; Delete;} 子句(AppendOrNot 规则,决定 removeObject/removeTool)。

3.11.3 GeoEntity 维度约束的实际使用

前面提到 GeoEntity123/GeoEntity12/GeoEntity012。它们在规则里出现的位置决定了"哪些命令 接受哪个维度的实体"。例如 Physical 用 GeoEntity(0--3 维)、某些网格命令用 GeoEntity123(1--3 维)。维度合法性在解析期就被语法结构保证。

3.11.4 形状列表的转换工具

第 119--122 行声明了三个转换函数:

cpp 复制代码
void ListOfShapes2VectorOfPairs(List_T *list, std::vector<std::pair<int, int>> &v);
void VectorOfPairs2ListOfShapes(const std::vector<std::pair<int, int>> &v, List_T *list);
void ListOfDouble2Vector(List_T *list, std::vector<int> &v);
void ListOfDouble2Vector(List_T *list, std::vector<double> &v);

ListOfShapes2VectorOfPairs / VectorOfPairs2ListOfShapes 是语法层的 List_T 与内核层的 std::vector<pair<int,int>> 之间的翻译器。所有几何命令都靠它们跨越"解析器容器"与"内核容器" 的鸿沟。

3.12 脚本桥:scriptStringInterface

有一条非脚本文件路径 也会进入解析器:用字符串拼出一条命令,然后解析执行 。 src/geo/scriptStringInterface.cpp 提供 scriptAddCommand(第 30--127 行):把外部传入的命令字符串写入临时文件、调用 gmsh_yyparse() 解析、再同步 GEO/OCC 内部状态。

它的用途 :GUI 操作、插件、API 都能用最少胶水复用整门脚本语言的表达能力。例如 GUI 里 "画一个圆"可能就拼成一条 Circle(...) 命令再解析执行。

它与第 3.10 节的"直接调 GEO_Internals::addCircleArc"形成两条互补路径:

  • 少量、简单命令 → 直接调内核 API(快、类型安全);
  • 复杂、可扩展命令 → 拼接脚本字符串再解析(复用整门语言)。

3.13 脚本语言能力全景

第 3.4.2 节按 token 分组列了 GEO 语言的"词汇"。这里按"能做什么"给每类配上用法与内核落点。

3.13.1 数学与常量

GEO 提供完整的数学函数集(tExp/tLog/tLog10/tSqrt/tSin/tAsin/tCos/tAcos/tTan/tAtan/tAtan2/ tSinh/tCosh/tTanh/tFabs/tAbs/tFloor/tCeil/tRound/tMin/tMax/tFmod/tModulo/tHypot), 外加保留常量 Pi(tPi)与并行量 MPI_Rank/MPI_Size(tMPI_Rank/tMPI_Size)。

示例:

geo 复制代码
x = Cos(45 * Pi / 180);
n = Max(2, Floor(10 / lc));
r = MPI_Rank == 0 ? 1.0 : 2.0;   // 并行下按 rank 分支

落点 :语义动作直接调用 C 标准库或 Numeric 工具;Pi 在词法层就是保留 token。 MPI_Rank 让脚本在并行/多进程场景下自适应。

3.13.2 字符串处理

tStr/tSprintf/tStrCat/tStrPrefix/tStrRelative/tStrReplace/tStrSub/tStrLen/ tFind/tStrFind/tStrCmp/tStrChoice/tUpperCase/tLowerCase。

示例:

geo 复制代码
name = Sprintf("part_%03d", 7);      // → "part_007"
p = StrPrefix(name, "_");             // 前缀
s = StrReplace(name, "part", "block");
len = StrLen(name);

落点 :这些函数在语义动作里转调 StringUtils(src/common/StringUtils.h)。 字符串变量存在 gmsh_yystringsymbols(第 3.6.1 节)。

3.13.3 列表生成

tList(普通列表)、tLinSpace(等差数列)、tLogSpace(等比数列)、tListFromFile (从文件读一组数)。

示例:

geo 复制代码
zs[] = LinSpace{0, 1, 11};           // 0 到 1 步长 11 段

落点 :返回 List_T *(<l> 语义值),可赋给列表变量,供 For 遍历。这是"数据驱动建模" 的基础。

3.13.4 文件与强制取值

  • tExists:变量是否存在;
  • tFileExists:文件是否存在;
  • tGetForced / tGetForcedStr:取变量值,未定义时用默认。

示例:

geo 复制代码
FileExists("mesh.msh") ? 1 : 0;
h = GetForcedNumber("h", 0.1);      // 未定义则 0.1

落点 :GetForced 的四种规则(第 5022--5034 行)让"读一个可能未定义的参数"成为单条表达式 ------这正是参数化脚本(同一脚本用不同参数组合运行)的关键能力。

3.13.5 系统信息

tCpu(CPU 时间)、tMemory/tTotalMemory(内存)、tGMSH_MAJOR/MINOR/PATCH_VERSION (版本号)、tCurrentDirectory/tCurrentFileName/tGmshExecutableName(路径)、 tToday(日期)。示例:

geo 复制代码
Msg = Sprintf("Gmsh %d.%d.%d", GMSH_MAJOR_VERSION, GMSH_MINOR_VERSION, GMSH_PATCH_VERSION);

落点 :转调 OS/CreateFile(src/common/OS.h)与版本宏(第 1 章 CMakeLists.txt 注入)。

3.13.6 可视化辅助

tDraw(触发绘制)、tShow/tHide(显示/隐藏实体)、tColor/tColorTable(颜色)、 tText2D/tText3D(标注)、tBoundingBox(包围盒选择)、tInterpolationScheme(插值方案)。

示例:

geo 复制代码
Color Grey{ Point{1}; }
Show "*";          // 显示全部
Hide "*";
Color Red{ Surface{1}; }

落点 :这些命令写 GEntity::setColor/setVisibility 与视图选项(第 1 章 1.8.5 节)。

3.13.7 物理量查询

tMass(质量/体积)、tCenterOfMass(质心)、tMatrixOfInertia(惯性张量)、tNormal (法向)、tCurvature(曲率)、tDistanceFunction(距离函数)。

示例:

geo 复制代码
v = Volume{1};   // 体积
c = CenterOfMass{ Volume{1} };

落点 :GRegion::computeSolidProperties(第 2.18 节)与 GFace::curvatures(第 2.17.2 节)。

3.13.8 拓扑操作

tCreateTopology/tCreateGeometry(构建拓扑)、tClassifySurfaces(分类面)、 tHomology/tCohomology/tBetti(同调/上同调/Betti 数)、tRenumberMeshNodes/ tRenumberMeshElements/tCreateMeshEdges/tCreateMeshFaces。

示例:

geo 复制代码
CreateTopology;                      // 从网格反推拓扑(第 2.12 节)
b0 = Betti[0]; b1 = Betti[1];        // Betti 数

落点 :GModelCreateTopologyFromMesh(第 2.12 节)与 Homology(第 8 章)。

3.13.9 网格相关命令

tMeshAlgorithm(算法选择)、tMeshSize(目标尺寸)、tMeshSizeFromBoundary、tRecombine (重组合)、tTransfinite(结构化)、tRefineMesh/tRecombineMesh/tAdaptMesh/ tTransformMesh/tRelocateMesh/tReorientMesh、tLayers/tScaleLast/tHole(拉伸/边界层)、 tQuadTriAddVerts/tQuadTriNoNewVerts(四边形-三角形过渡)、tSetOrder(高阶)。

示例:

geo 复制代码
Mesh.Algorithm = 6;                  // 显式指定二维算法
Transfinite Curve{1} = 10;           // 边上 10 个点(第 4 章)
Recombine Surface{1};                // 面重组合成四边形

落点 :这些命令写 CTX::instance()->mesh.*、GEntity::meshAttributes(第 2.16.1 节), 或调用 RefineMesh/AdaptMesh(第 7 章)。

3.13.10 Onelab 交互

tOnelabAction/tOnelabRun/tSetChanged/tCodeName。

示例:

geo 复制代码
OnelabRun("solver");                        // 启动外部求解器(第 10 章)
DefineNumber("param", 1.0);                 // 定义 Onelab 参数

落点 :Onelab/onelabUtils(第 1 章 1.15.2 节),通过 socket 与外部交互。

3.13.11 视图与插件

tPlugin(调用插件,第 9 章)、tNewView(新建视图)、tTime(时间步)、tCombine (合并视图)。

示例:

geo 复制代码
Plugin("CutPlane").A = 0; ...; Plugin("CutPlane").Run;

落点 :PluginManager::action(第 9 章 9.7.3 节)。

3.13.12 其它核心命令

  • tSetFactory:切换几何内核(第 3.1.3 节);
  • tAlias/tAliasWithOptions/tCopyOptions:命令别名与选项复制;
  • tDefineConstant/tUndefineConstant:常量定义;
  • tSetPartition:分区(第 7 章);
  • tUnsplitWindow/tSyncModel/tNewModel:模型/窗口管理。

3.14 解析错误与调试

3.14.1 错误报告

解析器用三个机制报错(Gmsh.y 第 105--109 行声明):

cpp 复制代码
void yyerror(const char *s);            // flex/bison 的标准错误回调
void yymsg(int level, const char *fmt, ...);   // 带级别的消息(level 0 = 错误)
char *strsave(char *ptr);

yymsg(0, ...) 用于非致命错误 (如第 3.10.2 节的"OCC 不支持 twist")------它打印消息但不 中断解析(0 级)。这正是为什么 Gmsh 能在遇到问题时继续解析脚本、收集多个错误。

全局变量 gmsh_yyerrorstate(Parser.h 第 441 行)记录错误状态。

3.14.2 符号打印

PrintParserSymbols(bool help, std::vector<std::string> &vec)(Parser.h 第 447 行)把当前 所有变量/结构体以文本形式列出。它服务于 - 命令行选项(如 -parse/-v)------用户可以用 它调试脚本里到底定义了什么变量。

3.14.3 调试技巧

  • Print/Printf (token tPrintf):打印变量值;
  • Error/Warning (token tError/tWarning):主动抛错/警告(用于断言);
  • TestLevel (token tTestLevel):脚本里按"测试级别"分支(Gmsh 的测试脚本用它)。

示例:

geo 复制代码
If(TestLevel >= 2)
  Error("This must not happen at level %g", TestLevel);
EndIf

3.15 GEO 与 Python 脚本的对照

同一件事在两种语言里的写法对比,能突出 GEO 的定位:

目的 GEO 脚本 Python(API)
初始化 (无需,直接运行 .geo) gmsh.initialize()
建点 Point(1) = {0,0,0,lc}; gmsh.model.geo.addPoint(0,0,0,lc,1)
建线 Line(1) = {1,2}; gmsh.model.geo.addLine(1,2,1)
建面 Curve Loop(1)={...}; Plane Surface(1)={1}; addCurveLoop([...],1); addPlaneSurface([1],1)
同步 (自动/SyncModel) gmsh.model.geo.synchronize()
生成网格 Mesh 2; gmsh.model.mesh.generate(2)
保存 Save "m.msh"; gmsh.write("m.msh")
循环 For i In {1:10} ... EndFor for i in range(1,11): ...
宏 Macro f ... Return; Call f; Python 函数

核心区别:

  1. GEO 是声明式 + 自动同步 :写几何命令时是"配置内核容器",同步由 Gmsh 负责(第 3.11 节); Python 需要显式 synchronize()。
  2. GEO 的 (dim,tag) 寻址内建在语法里 (GeoEntity 等);Python 用显式 (dim,tag) 元组。
  3. GEO 无函数作用域 :宏是无参文本替换 + fgetpos 跳转;Python 有真正的函数与局部变量。
  4. GEO 的 newp/newc/... 是语言级自动编号 ;Python 需要自己维护计数器或传 -1 让 Gmsh 分配。

理解这些差异,就能明白为什么Gmsh 同时保留两种语言:GEO 更适合"几何密集、结构规整"的 建模;Python 更适合"逻辑复杂、与外部库耦合"的工作流。

3.16 性能与最佳实践

  1. 循环体尽量短 :因为 For 每轮重读文本(第 3.8 节),长循环体开销大。把复杂逻辑放进 宏,循环里只 Call。
  2. 避免在循环里做重解析 :skip/回读是 O(文本长度);大循环 + 大循环体 = 慢。
  3. 优先用 (dim,tag) 精确寻址 :Physical .../Delete 等命令用具体 tag 比用 BoundingBox/Recursive 匹配更快。
  4. SetFactory 尽早调用:切换内核会重建内核状态,应在脚本开头切换。
  5. 用 GetForced 做参数化(第 3.13.4 节):让同一脚本接受不同外部参数,避免复制脚本。
  6. 善用 Alias/CopyOptions:把常用命令序列或视图选项复用,减少重复。

3.17 常见陷阱

  1. : 与 ... 等价 (第 3.3.4 节):两者都是 tDOTS,写法不影响语义。
  2. newp 与 newc 默认可能返回相同数字 (OldNewReg=1):因为点一个计数器、其余一个。 但 tag 只在维度内唯一,所以无害(第 3.3.3 节)。
  3. 循环变量的作用域 :For i In {1:10} 的 i 在循环外仍可见(全局符号表)。
  4. 宏不是函数 :宏体是文本替换 + 位置跳转,没有参数 (参数靠外部变量传递,如 t5.geo 的 x/y/z/r)。
  5. Extrude 的输出是列表 :不要假设它返回单个实体;要用 out[] = ...。
  6. 错误不总中断 :yymsg(0, ...) 类错误会继续解析(第 3.14.1 节),脚本可能在"部分失败" 状态下继续执行------检查错误计数。

3.18 深入解析器的实现细节

3.18.1 Gmsh.y 的结构

src/parser/Gmsh.y 是全书最大文件之一,结构分为四段:

  1. %{ ... %} 头部 (第 1--168 行):C 头文件包含、全局变量定义、辅助函数声明。它直接 嵌入生成的 Gmsh.tab.cpp,因此可以在这里定义静态变量、包含头文件。
  2. %union + %token + %type(第 170--270 行):语义值类型、token、非终结符类型。
  3. %% ... %% 规则区:全部文法规则与语义动作(本章反复引用)。
  4. 尾部函数定义 :yyerror/yymsg/skip/skipTest/assignVariable/treat_Struct_* 等 辅助函数(第 105--162 行声明的那些)的实现。

关键点:头部(第 67--97 行)定义了本章提到的全部全局解析器变量:

cpp 复制代码
std::string gmsh_yyname;
int gmsh_yyerrorstate = 0;
int gmsh_yyviewindex = 0;
std::map<std::string, gmsh_yysymbol> gmsh_yysymbols;
std::map<std::string, std::vector<std::string>> gmsh_yystringsymbols;
std::string gmsh_yyfactory;
NameSpaces gmsh_yynamespaces;

static std::vector<double> ViewCoord;
static std::vector<double> *ViewValueList = 0;
static std::vector<std::string> ViewStrings;
static std::vector<double> ViewTime;
static ExtrudeParams extr;
static gmshSurface *myGmshSurface = 0;
static int statusImbricatedTests[MAX_RECUR_TESTS];
static int ImbricatedLoop = 0, ImbricatedTest = 0;
static fpos_t yyposImbricatedLoopsTab[MAX_RECUR_LOOPS];
static int yylinenoImbricatedLoopsTab[MAX_RECUR_LOOPS];
static double LoopControlVariablesTab[MAX_RECUR_LOOPS][3];
static std::string LoopControlVariablesNameTab[MAX_RECUR_LOOPS];
static std::string struct_name, struct_namespace;
static int dim_entity;
static std::map<std::string, std::vector<double>> floatOptions;
static std::map<std::string, std::vector<std::string>> charOptions;
static int flag_Enum, member_ValMax;

这些静态变量分几类:

  • 符号与内核 :gmsh_yysymbols/gmsh_yystringsymbols/gmsh_yynamespaces/gmsh_yyfactory;
  • 命令参数暂存 :extr(拉伸参数)、floatOptions/charOptions(结构体成员暂存)、 ViewCoord/ViewValueList/ViewStrings/ViewTime(后处理视图构造)、myGmshSurface (球面片);
  • 控制流状态 :ImbricatedLoop/ImbricatedTest 等(第 3.8 节);
  • 结构体构造状态 :struct_name/struct_namespace/dim_entity。

为什么这么多全局静态变量? 因为 bison 的 %union 只能放小对象,而许多命令需要跨多个 语义动作传递大对象(如 ExtrudeParams extr)。把这些放在头部的静态变量里,是"用全局状态 弥补 bison 语义值容量有限"的务实做法。

3.18.2 变量操作的实现函数

Gmsh.y 尾部实现了一批变量操作函数(第 113--122 行声明):

cpp 复制代码
void assignVariable(const std::string &name, int index, int assignType, double value);
void assignVariables(const std::string &name, List_T *indices, int assignType, List_T *values);
void incrementVariable(const std::string &name, int index, double value);
int printListOfDouble(const char *format, List_T *list, std::string &buffer);
fullMatrix<double> ListOfListOfDouble2Matrix(List_T *list);
void ListOfDouble2Vector(List_T *list, std::vector<int> &v);
void ListOfDouble2Vector(List_T *list, std::vector<double> &v);
void ListOfShapes2VectorOfPairs(List_T *list, std::vector<std::pair<int, int>> &v);
void VectorOfPairs2ListOfShapes(const std::vector<std::pair<int, int>> &v, List_T *list);

assignVariable(name, index, assignType, value):

  • name:变量名;
  • index:索引 (a[3] = ... 的 3;对无索引赋值是 0);
  • assignType:赋值类型(普通 / += / -= / *= / /=);
  • value:值。

index 参数是理解 GEO 列表变量的关键 :a[] = {...} 与 a[3] = v 都通过它表达。 assignVariables 把"索引列表 + 值列表"成对赋值(如 a[] = {1,2,3})。

ListOfShapes2VectorOfPairs/VectorOfPairs2ListOfShapes 是第 3.11.4 节的转换器。

3.18.3 表达式规则的实现

FExpr(浮点表达式)的规则覆盖:

  • 算术:加减乘除、取模、幂;
  • 比较 :< > <= >= == !=;
  • 逻辑 :&& || !;
  • 位运算 :<< >> & | ^;
  • 三元 :cond ? a : b;
  • 函数调用 :Sin(x)/Min(a,b) 等;
  • 常量与变量 :字面量、Pi、MPI_Rank、符号表查询。

VExpr(向量)是逗号分隔的 FExpr 序列,语义值用 double v[5](第 3.4.1 节)。这就是 为什么几何命令能写 {dx, dy, dz}------语法层把它构造为一个向量语义值。

3.18.4 结构体成员访问函数

第 3.7.2 节的 treat_Struct_FullName_* 系列函数(Gmsh.y 第 144--162 行声明)处理"三级成员 访问":

  • treat_Struct_FullName_Float(c1, c2, type_var, index, val_default, type_treat):读/写 ns::struct 的成员,返回 double;
  • treat_Struct_FullName_dot_tSTRING_Float(c1, c2, c3, ...):读/写 ns::struct.member;
  • treat_Struct_FullName_dot_tSTRING_ListOfFloat(...):读成员向量;
  • 对应的 _String 版本处理字符串成员。

命名里的 c1/c2/c3 分别是"命名空间 / 结构体名 / 成员名",type_treat 区分"读还是写"。 这套函数把 NameSpaces 的三级寻址封装成几个入口,语法规则只需调用它们。

3.19 解析相关的内存管理

3.19.1 List_T 的分配与释放

语法层的列表用 List_T(src/common/ListUtils.h)。它是 C 风格 容器:

  • List_New/List_Add/List_Nbr/List_Read/List_Reset/List_Delete;
  • 用 void* 存任意类型,需要配套的 Free* 回调。

第 3.10.1 节里 List_Reset($$) 就是"清空列表以复用"。必须显式管理生命周期 ------不像 std::vector 有 RAII。因此语法动作里到处是 List_Reset/List_Delete,这是"用 C 容器"的 代价。

3.19.2 strsave 与字符串所有权

strsave(char *ptr)(第 108 行声明)把字符串复制一份保存,避免悬垂指针。词法层的 parsestring 返回的字符串需要 strsave 才能安全存活到语义动作------因为 yytext 缓冲会被 覆盖。这是 flex/bison 输出给调用者的经典陷阱 ,Gmsh 用 strsave 规避。

3.19.3 MallocUtils

MallocUtils(src/common/MallocUtils.h)是 Gmsh 的内存分配 封装(Malloc/Realloc/Free 的包装)。解析器大量用它配合 List_T。

3.20 解析器的并发与重入

3.20.1 全局状态的限制

由于 flex/bison 生成的代码依赖全局状态(yyin/yytext/yylineno)与本章列出的众多全局 变量,Gmsh 的解析器不是可重入的:

  • 同一时刻只能有一个 gmsh_yyparse 在跑;
  • gmsh_yysymbols/FunctionManager 等是全局的,多个脚本会共享。

这解释了为什么 GUI(主线程)与后台脚本不能同时解析。第 4 章的网格生成用 CTX->lock 串行化,与此是同一类约束。

3.20.2 scriptAddCommand 的临时文件

第 3.12 节的 scriptAddCommand 之所以"写入临时文件再解析",而不是直接喂给解析器,正是 为了避免打乱当前解析上下文 :临时文件给了它一个独立的 yyin,解析完再恢复。这是一个 "用文件隔离重入"的实用技巧。

3.21 一个脚本的完整解析轨迹:t5.geo 的 CheeseHole

以 tutorials/t5.geo 里的 Macro CheeseHole 为例(节选):

geo 复制代码
Macro CheeseHole
  p1 = newp; Point(p1) = {x,  y,  z,  lcar3};
  p2 = newp; Point(p2) = {x+r,y,  z,  lcar3};
  ...
  c1 = newc; Circle(c1) = {p2,p1,p7}; c2 = newc; Circle(c2) = {p7,p1,p5};
  ...
  l1 = newcl; Curve Loop(l1) = {c5,c10,c4};
  ...
  s1 = news; Surface(s1) = {l1};
  ...
Return

解析轨迹:

  1. 定义阶段 :Macro CheeseHole 规则(第 3497 行)触发 FunctionManager::createFunction, 把宏体在 t5.geo 里的起始位置 记入 functions;解析器此时跳过宏体 (因为不是 Call)。
  2. 调用阶段 :脚本某处 Call CheeseHole;(第 3519/3526 行的规则)→ FunctionManager::enterFunction 压栈并把 yyin 切到宏体。
  3. 宏体内解析 :
    • p1 = newp → 词法层的 newp 立即返回一个新点 tag(tDOUBLE),赋值给 p1;
    • Point(p1) = {x,y,z,lcar3} → tPoint 规则调用 GEO_Internals::addVertex(p1, x,y,z,lcar3) (第 2.3.2 节);
    • Circle(c1) = {p2,p1,p7} → addCircleArc(c1, p2, p1, p7)(圆心 p1);
    • Curve Loop(l1) = {...} → addCurveLoop(l1, {...});
    • Surface(s1) = {l1} → addSurfaceFilling(s1, {l1}, sphereCenterTag)(第 2.3.2 节, 这里退化成球面片)。
  4. 返回阶段 :Return → FunctionManager::leaveFunction 弹栈并恢复 yyin。

注意宏体里引用的 x/y/z/r/lcar3 都是外部变量 ------宏没有参数,靠外部变量传值。调用方先 设好这些变量再 Call。

这条轨迹把第 3 章的机制(词法自动编号、语法动作、宏、内核调用)全部串了一遍。

3.22 扩展 GEO 语言的成本与做法

如果你要给 GEO 语言加一条新命令,需要动的地方(以加一条 Foo(1) = {...} 为例):

  1. 词法层 (Gmsh.l):加一条 Foo return tFoo;(若有关键字别名也在这里加);
  2. 语法层 token (Gmsh.y 的 %token):加 tFoo;
  3. 语法规则 (Gmsh.y 规则区):加 tFoo '(' FExpr ')' '=' '{' FExpr ',' ... '}' 的规则与 语义动作,动作里调用内核/实体 API;
  4. 若需新函数 :在 %{...%} 声明 + 尾部实现;
  5. 重新生成 :用 flex/bison 重新生成 Gmsh.yy.cpp/Gmsh.tab.cpp。

成本 :因为生成物入库,改 .l/.y 后必须重新生成并提交两个大文件(它们是全项目最大的 文本文件之一),diff 很大。收益:语言的表达能力与内核能力一一对应,用户无需写 C++ 就能 用新功能。

这是 Gmsh "把能力暴露给脚本"的固定流程。第 9 章的插件作者若想让插件可被脚本调用,也要走 类似的"加 token/规则"路径(或在插件内部用 scriptAddCommand 绕过)。

3.23 与其它 DSL/嵌入语言对比

系统 脚本语言 解析方式 与 Gmsh 的异同
Gmsh GEO flex+bison,手工 DSL 语法层级 = 几何层级;自动编号;回读循环
OpenSCAD 自有语言 递归下降 同为"几何 DSL";OpenSCAD 有真正的函数与 AST
FreeCAD Python 解释器 走通用语言 API(与 Gmsh 的 Python 路径类似)
Abaqus INP(关键字) 关键字解析 纯数据格式,无控制流
Netgen Tcl/Python 解释器 通用脚本语言驱动几何
TetGen 无脚本(配置) 参数解析 无 DSL,纯工具

Gmsh 的独特之处 :它既不像 OpenSCAD 那样有完整 AST/函数,也不像 FreeCAD/Netgen 那样依赖 通用语言。它自造了一门"刚好够用"的 DSL:语法层级与几何层级一一对应、自动编号融入 表达式、循环靠回读、宏靠跳转。这种"够用就好"的设计,让解析器实现规模可控,同时保留了 足够的表达能力。

代价是:没有真正的函数作用域、没有 AST 优化、循环有性能开销。但对"几何建模脚本通常 不长"这一现实而言,这些代价可以接受。

3.24 本章小结

脚本语言与解析器的核心:

  1. 一門为几何而生的 DSL :语法层级(Point/Curve/Loop/Surface/Volume)与内核容器一一对应, (dim, tag) 寻址贯穿始终,%union 中的 Shape 与 List_T 是核心载体。
  2. 自动编号融入表达式 :newp/newc/... 在词法层就索取 tag 并当作数值(tDOUBLE)返回, 无需特殊语法。
  3. 类型检查下沉到文法 :GeoEntity/GeoEntity123/GeoEntity12/GeoEntity012 在解析期挡住维度 错误。
  4. 循环靠"回读"而非 AST :fgetpos/fsetpos + ImbricatedLoop 反复重解析循环体; MAX_RECUR_LOOPS/TESTS = 100 限嵌套;skip/skipTest 是必要补充。
  5. 宏用位置跳转 :FunctionManager 单例维护"函数表 + 调用栈",Macro/Call/Return 靠 fgetpos/fsetpos 切换 yyin;宏无参数,靠外部变量传值。
  6. 双内核在语法动作里分派 :gmsh_yyfactory 决定 GEO_Internals 还是 OCC_Internals, 并存在内核能力差异(如 OCC 不支持 twist)。
  7. 四条通用原则:双内核分派、可选中缀(默认值先清零)、同 token 多规则、输入列表复用为 输出。
  8. 全局状态换实现简单 :解析器大量依赖全局变量与静态暂存,因而不可重入;scriptAddCommand 用临时文件隔离。

第 4 章进入网格层,看 GenerateMesh 如何把这些几何数据变成一维到三维的网格。

关键词

Gmsh.l、Gmsh.y、gmsh_yyparse、gmsh_yylex、gmsh_yyfactory、gmsh_yysymbols、 gmsh_yystringsymbols、gmsh_yynamespaces、%union、Shape、List_T、newp/newc/newcl/news/newv/newf、 tDOUBLE、FExpr、VExpr、StringExpr、GeoEntity、ImbricatedLoop、MAX_RECUR_LOOPS、 skip/skipTest、FunctionManager、Macro/Call/Return、ExtrudeParameters、 ListOfShapes2VectorOfPairs、treat_Struct_FullName_*、Struct/Structs/NameSpaces、 scriptAddCommand、yymsg、PrintParserSymbols。

3.25 Shape 与 GeoDefines 编号体系

3.25.1 Shape:(类型, 编号) 二元组

第 3.4.1 节说 %union 里有 Shape s。它的定义在 src/geo/GeoDefines.h(第 9--13 行):

cpp 复制代码
class Shape {
public:
  int Type;
  int Num;
};

极简:Type 是几何类型编码,Num 是实体 tag 。所有几何命令的输入输出列表都是 Shape 的列表(List_T)。

3.25.2 类型编码:type / 100 = 维度 + 1

GeoDefines.h(第 15--46 行)定义了一套**"用百位表维度"的编码**:

cpp 复制代码
// type/100 gives (dimension+1)
#define MSH_POINT          100
#define MSH_POINT_BND_LAYER 101
#define MSH_POINT_DISCRETE  102

#define MSH_SEGM_LINE      200
#define MSH_SEGM_SPLN      201
#define MSH_SEGM_CIRC      202
#define MSH_SEGM_CIRC_INV  203
#define MSH_SEGM_ELLI      204
#define MSH_SEGM_ELLI_INV  205
#define MSH_SEGM_LOOP      206
#define MSH_SEGM_BSPLN     207
#define MSH_SEGM_NURBS     208
#define MSH_SEGM_BEZIER    209
#define MSH_SEGM_BND_LAYER 211
#define MSH_SEGM_DISCRETE  212

#define MSH_SURF_PLAN      300
#define MSH_SURF_REGL      301
#define MSH_SURF_TRIC      302
#define MSH_SURF_BND_LAYER 303
#define MSH_SURF_LOOP      304
#define MSH_SURF_DISCRETE  305

#define MSH_VOLUME         400
#define MSH_VOLUME_DISCRETE 401

#define MSH_PHYSICAL_POINT   500
#define MSH_PHYSICAL_LINE    501
#define MSH_PHYSICAL_SURFACE 502
#define MSH_PHYSICAL_VOLUME  503

这套编码很聪明 :type / 100 直接给出维度 + 1:

  • 100/100 = 1,所以 100(点)维度是 0;
  • 200/100 = 2,所以 200(线)维度是 1;
  • 300/100 = 3,维度 2;
  • 400/100 = 4,维度 3;
  • 500/100 = 5,物理组(不是几何实体,但用同一编码)。

于是"从 Shape::Type 得到维度"只需一个整数除法。类似地,% 100 给出子类型(00=普通、 01=样条、02=圆、06=环、07=BSpline、...)。

注意这组编号与第 1 章的 MSH_* 单元编号(MSH_LIN_2=1 等)是两套不同的东西 :这里的 MSH_POINT/MSH_SEGM_* 是脚本层的几何类型编码 ,用于 Shape;第 1 章的 MSH_LIN_2 是 网格单元类型编码 ,用于文件格式。两者都带 MSH_ 前缀,容易混淆------记法提示:几何编码 是三位数(100/200/300/400/500),单元编码是两位数(1..140)。

3.25.3 MSH_SEGM_* 与曲面子类型

线类型的子类完整映射自研内核的曲线种类:

编码 含义 对应内核
MSH_SEGM_LINE 直线 addLine
MSH_SEGM_SPLN 样条 addSpline
MSH_SEGM_CIRC 圆弧 addCircleArc
MSH_SEGM_CIRC_INV 反向圆弧 圆弧(反向)
MSH_SEGM_ELLI 椭圆弧 addEllipseArc
MSH_SEGM_LOOP 曲线环 addCurveLoop
MSH_SEGM_BSPLN BSpline addBSpline
MSH_SEGM_NURBS NURBS BSpline 特例
MSH_SEGM_BEZIER Bezier addBezier
MSH_SEGM_BND_LAYER 边界层曲线 边界层
MSH_SEGM_DISCRETE 离散曲线 STL/网格导入

曲面子类型同理(MSH_SURF_PLAN=平面、REGL=规则面、TRIC=三角面、BND_LAYER=边界层面、 LOOP=曲面环、DISCRETE=离散面)。这张表就是自研内核"能表达哪些几何"的目录 (第 2 章 的 GEO_Internals::addXxx 与之一一对应)。

3.26 词法层的更多细节

3.26.1 yytext 与 yylval

flex 提供两个全局:yytext(当前匹配的文本)与 yylval(语义值,即 %union 的实例,生成的 名字是 gmsh_yylval)。词法规则通过写 gmsh_yylval.d = ... 传递数值:

text 复制代码
"newp"   { gmsh_yylval.d = NEWPOINT(); return tDOUBLE; }

而字符串 token(tSTRING/tBIGSTR)通过 strsave(yytext) 传递字符串。

3.26.2 数字与标识符的识别

第 3.3.6 节的模式定义了标识符与数字。数字的完整模式覆盖:

  • 整数(digit+);
  • 浮点(digit* '.' digit+,以及 .digit+);
  • 科学计数(... exp,exp = [Ee][-+]?digit+)。

注意 . 既是小数点又是 ...(tDOTS)的一部分 ------flex 的"最长匹配"规则会正确区分 1.5 与 1...10(后者匹配 ... 时数字模式只吃 1,然后 ... 作为独立 token)。这是 "最长匹配 + 顺序优先"的经典应用。

3.26.3 关键字表的组织

第 94 行之后的关键字规则是按字母序排列 的(Abort/Abs/AbsolutePath/Acos/...)。 这种做法便于人工维护与查找,也是 Gmsh 一贯的风格(第 1 章 CMakeLists 的 opt() 也是字母序)。

每条规则形如:

text 复制代码
Acos                    return tAcos;

即"匹配到 Acos 就返回 tAcos"。别名(第 3.3.5 节)则表现为多条规则指向同一 token:

text 复制代码
ArcCos                  return tAcos;
Acos                    return tAcos;

3.26.4 行号与列号

flex 默认维护 yylineno(在 YY_INPUT 里自增,第 3.3.1 节)。行号用于 yymsg 报错定位。 列号没有维护------因为 Gmsh 的错误粒度通常到"命令"即可,不需要精确到列。

3.27 语法规则的完整分类

Gmsh.y 的规则区按"命令族"组织。了解这个组织,就能快速找到某条命令的规则。

3.27.1 赋值与变量

  • NumericAffectation/NumericIncrement(数值赋值/自增,第 231 行声明为 <i>);
  • DefineStruct/NameStruct_Arg;
  • DefineConstant/UndefineConstant;
  • 字符串赋值(StringExpr)。

3.27.2 几何图元

  • 点:tPoint(第 202 行 token);
  • 曲线:tCircle/tEllipse/tCurve/tSpline/tBSpline/tBezier/tNurbs/tCatenary;
  • 曲面:tSphere/tPolarSphere/tSurface/tPlane/tRuled;
  • 体:tVolume;
  • 基本体:tBox/tCylinder/tCone/tTorus/tEllipsoid/tQuadric/tWedge;
  • 辅助:tRectangle/tDisk/tWire。

3.27.3 拓扑与关系

  • 环/壳:Curve Loop/Surface Loop(无独立 token,靠 tCurve+tGeoEntity 组合);
  • tPhysical(物理组,第 2629/2649/2708 行);
  • tCompound/tPeriodic/tEmbedded?/tParent/tUsing;
  • tTransfinite(结构化,TransfiniteType/TransfiniteArrangement 子规则)。

3.27.4 变换(Transform 规则族)

第 2365 行起:

c 复制代码
Transform :
    tTranslate VExpr '{' MultipleShape '}'
    ...
  | tRotate  ...
  | tSymmetry ...
  | tDilate  ...
  | tExtrude ...        // 见第 3.10 节
  | tAffine  ...
  | tClosest ...
  | tIntersect ...
;

所有变换命令都归到 Transform 这个非终结符 ,这样它们只在"语句"级别出现,而不像 几何图元那样能在表达式里求值(因为变换的"结果"是隐式的)。MultipleShape 是"多个形状组" 的规则(用于 Translate { ... } { ... } 这种"参数块 + 形状块"结构)。

3.27.5 布尔与局部修饰

  • tBooleanUnion/tBooleanIntersection/tBooleanDifference/tBooleanSection/ tBooleanFragments/tThickSolid(第 211--212 行 token);
  • tFillet/tChamfer/tSplit/tDelete/tCoherence/tHealShapes。

AppendOrNot 子规则(第 234 行声明为 <i>)处理 {...}/{...; Delete;} 两种子句。

3.27.6 网格命令

  • 算法/尺寸:tMeshAlgorithm/tMeshSize/tMeshSizeFromBoundary;
  • 重组合:tRecombine/RecombineAngle/tSmoother;
  • 结构化:tTransfinite;
  • 细化/适应:tRefineMesh/tAdaptMesh/tTransformMesh/tRelocateMesh/tReorientMesh/ tRecombineMesh;
  • 阶数:SetOrder(token tSetOrder);
  • 拉伸层:tLayers/tScaleLast/tHole;
  • 四边形过渡:tQuadTriAddVerts/tQuadTriNoNewVerts/tRecombLaterals/tTransfQuadTri。

3.27.7 尺寸场(Field)

tField + 一个专门的规则族,处理:

geo 复制代码
Field[1] = Box;
Field[1].VIn = 0.1;
Field[1].VOut = 0.5;
Field[2] = Threshold;
Field[2].InField = 1;
Background Field = 2;

落点 :GModel::getFields()(FieldManager,第 4 章)→ newField(id, type) + getOption(name) 设值。Field 规则要处理"方括号索引"与"点号成员"的解析。

3.27.8 视图(PView)

tNewView/tTime/tCombine/tText2D/tText3D/tInterpolationScheme + 视图选项。 规则要把"视图构造"的坐标与值暂存到第 3.18.1 节的 ViewCoord/ViewValueList/ViewStrings/ ViewTime 静态变量,最后一次性建成视图。

3.27.9 控制与元编程

tFor/tIf/tMacro/tCall/tReturn/tExit/tAbort/tShow/tHide/tSlide/tAlias/ tAliasWithOptions/tCopyOptions。

3.27.10 系统与 Onelab

tCpu/tMemory/tTotalMemory/tToday/版本 token/路径 token/tOnelabAction/tOnelabRun/ tSetChanged/tCodeName/tSetPartition/tSyncModel/tNewModel/tUnsplitWindow。

3.27.11 拓扑与文件查询

tCreateTopology/tCreateGeometry/tClassifySurfaces/tHomology/tCohomology/tBetti/ tExists/tFileExists/tGetForced/tGetForcedStr/tGetValue/tGetStringValue/tGetEnv/ tGetString/tGetNumber/tUnique/tSetMaxTag。

3.28 Field 脚本的解析轨迹

以第 4 章会用的尺寸场脚本为例,看解析器如何落地:

geo 复制代码
Field[1] = Box;
Field[1].VIn = 0.1;
Field[1].VOut = 0.5;
Field[2] = Threshold;
Field[2].InField = 1;
Field[2].SizeMin = 0.02;
Background Field = 2;

解析轨迹:

  1. Field[1] = Box; → tField 规则识别"字段定义",调用 GModel::current()->getFields()->newField(1, "Box")(第 4 章 FieldManager::newField)。
  2. Field[1].VIn = 0.1; → 识别"字段 id + 成员名 + 值",取 FieldManager 中 id=1 的字段, 调 field->getOption("VIn")->numericalValue(0.1)(第 4 章 FieldOption)。
  3. Field[2] = Threshold; → newField(2, "Threshold")。
  4. Field[2].InField = 1;/Field[2].SizeMin = 0.02; → 同上设成员。
  5. Background Field = 2; → FieldManager::setBackgroundFieldId(2)。

关键点 :Field[i].member = value 的解析涉及"方括号索引 + 点号成员"的复合访问,比普通变量 复杂。它由专门的规则 + Field 相关辅助函数处理。

3.29 视图脚本的解析

后处理视图可以用脚本直接构造(.pos 文件也走同一解析器):

geo 复制代码
View "myview" {
  ST(myview) = { {0,0,0,0.5}, {1,0,0,0.3} };   // 三角形带的标量
  T3(myview) = { {0,0,0,1}, {1,0,0,0.2} };
};

解析时,语义动作把坐标写入 ViewCoord、值写入 ViewValueList、字符串写入 ViewStrings、 时间写入 ViewTime(第 3.18.1 节的静态变量),最后一次性构造 PViewDataList(第 9 章)。 注意 .pos 与 .geo 共用同一解析器 ------这解释了第 1 章 CMake 选项里 PARSER "Enable GEO file parser (required for .geo/.pos scripts)" 的措辞。

3.30 bison 的优先级与冲突消解

GEO 语言里有两类天然歧义:

  1. 一元负号 vs 二元减号 :a = -1 与 a = b - 1。bison 用优先级声明(%left/%right/ %prec)解决,通常让一元负号优先级更高。
  2. :/... 在 For 与其它用途 :因为都映射到 tDOTS,规则上下文区分(第 3.3.4 节)。

Gmsh.y 的优先级声明(文件头部附近)为运算符指定结合性与优先级,从而让 bison 的 LALR(1) 分析器无需回溯即可解析。这是 Gmsh.y 能保持"单趟、无 AST"的基础。

3.31 错误恢复与鲁棒性

3.31.1 消息级别

yymsg(level, fmt, ...) 的 level 决定消息类型(Gmsh.y 第 107 行声明):

  • level 0:错误(Msg::Error);
  • level 1:警告(Msg::Warning);
  • 更高:信息/调试。

解析器的设计允许"非致命错误继续解析"------这样用户能在一次运行里看到多个问题,而不是 修一个报一个。典型例子:

  • yymsg(0, "GeoEntity dim out of range [0,3]")(第 2319 行):维度越界,报错但继续;
  • yymsg(0, "Twisting extrude not available with OpenCASCADE geometry kernel")(第 3644 行): 内核不支持,报错但继续;
  • yymsg(0, "Could not extrude shapes")(第 3615 行):操作失败,报错但继续。

3.31.2 错误计数与中止

Msg 的全局错误计数(第 1 章 1.15.1 节)与 CTX::instance()->abortOnError 配合:脚本模式 下,若某处设置 abortOnError=2,则在第一个错误后抛异常。解析器本身不决定是否中止 ------ 它只报告错误,由上层(GenerateMesh/脚本执行器)根据 abortOnError 决定。

3.31.3 语法错误的报告

yyerror(const char *s) 是 bison 的标准错误回调,在遇到语法错误时调用。它通常转调 yymsg 并带上当前行号(gmsh_yylineno)与 yytext,给出"在第 N 行附近期望 X"的提示。由于没有 AST,错误提示只能基于当前 token------这是无 AST 解析器的固有局限。

3.32 关键子规则的细节

Gmsh.y 里有一批子规则(非终结符),理解它们能更快读懂规则区。

3.32.1 LoopOptions / TransfiniteType / TransfiniteArrangement

Transfinite 命令的参数由这几个子规则解析:

  • TransfiniteType(<v>):结构化网格的"类型"(如比例、间距);
  • TransfiniteArrangement(<i>):点排列方式(两侧对称/单侧等);
  • LoopOptions(<d>):其它选项。

落点 :GEdge::meshAttributes.nbPointsTransfinite/typeTransfinite/coeffTransfinite (第 2.16.1 节)。

3.32.2 CircleOptions / EllipseOptions

Circle/Ellipse 的可选参数(法向 N、轴 V、圆角半径等)由 CircleOptions(<v>)解析。 落点:addCircleArc/addEllipseArc 的参数(第 2.3.2/2.7.4 节)。

3.32.3 AppendOrNot

布尔/局部修饰命令的 {...}/{...; Delete;} 子句由 AppendOrNot(<i>)解析,产出"是否 删除"的布尔。落点 :OCC_Internals::booleanXxx 的 removeObject/removeTool(第 2.8.3 节)。

3.32.4 HomologyCommand

Homology/Cohomology 命令的参数由 HomologyCommand(<c>)解析(把"域/子域/维度"编码成 字符串)。落点 :Homology 类(第 8 章)。

3.32.5 LP / RP 与结构体成员访问

LP(左括号)/RP(右括号)规则用于结构体成员的 [...] 索引。它们是第 3.7.2 节 treat_Struct_FullName_* 的语法入口。

3.32.6 ListOfListOfDouble2Matrix

ListOfListOfDouble2Matrix(List_T *list)(第 118 行声明)把"列表的列表"转成 fullMatrix<double> ------用于二维数组字面量 (如矩阵类型的参数)。落点:需要矩阵输入的命令(视图、变换矩阵)。

3.32.7 MultiStringExprVar / SurfaceConstraints

  • MultiStringExprVar(<l>):多字符串表达式(用于"多个名字/文件")。
  • SurfaceConstraints(<l>):曲面约束列表(用于 Surface 命令的附加约束)。

3.33 GEO 语言与 Tcl/Lua 的历史关系

Gmsh 早期曾提供 Tcl 与 Lua 脚本入口(历史上通过 Parser.h/FunctionManager 与外部 解释器集成)。当前代码里,GEO 是主语言,Python/Julia 是外部语言路径。历史痕迹仍在:

  • FunctionManager 的"函数表 + 调用栈"设计,原本同时服务于 Lua 函数与 GEO 宏;
  • Parser.h 里的全局符号表设计,与"给通用语言提供变量绑定"的需求相呼应。

理解这段历史有助于解释:为什么 GEO 语言既有"声明式几何命令",又有"字符串/列表/文件 操作"这类通用能力------因为它曾想成为"通用脚本的宿主语言",后来收敛为"几何 DSL + 少量通用 能力"。

3.34 同一功能的两条路径对照

第 3.12 节提到 scriptStringInterface。把"同一功能的两种实现路径"列成表,能清晰看到脚本层 与 API 层的关系:

功能 脚本路径 API/内核路径
建点 Point(1) = {...} GEO_Internals::addVertex / gmsh::model::geo::addPoint
建圆 Circle(1) = {...} addCircleArc
建面 Plane Surface(1) = {...} addPlaneSurface
布尔 BooleanUnion(...) OCC_Internals::booleanUnion
拉伸 Extrude {...} {...} GEO_Internals::extrude
生成网格 Mesh 2; gmsh::model::mesh::generate(2)
设选项 Mesh.Algorithm = 6; gmsh::option::setNumber("Mesh.Algorithm", 6)
调插件 Plugin("X") PluginManager::action("X", "Run", ...)

观察 :脚本命令与 API 函数几乎一一对应,因为它们最终落到同一批底层实现(第 1 章的 "API 是薄壳")。区别只在"入口语法":脚本走解析器 + 全局状态;API 走函数调用 + 显式参数。

这也解释了为什么 scriptAddCommand(字符串 → 解析)存在:它让 C++ 代码能复用"脚本入口", 而不必为每个功能都写一遍 API 调用。

3.35 解析器的测试

Gmsh 的解析器测试主要靠:

  • tutorials/*.geo:覆盖几何、宏、循环、尺寸场、视图等(第 2.37 节的"验证手段"同源);
  • examples/ 下的脚本:覆盖更复杂/边缘的组合;
  • TESTS (CMake 选项 ENABLE_TESTS):把教程脚本作为回归测试,比对输出网格/视图。

对解析器改动的验证要点:

  1. 语法改动后跑全教程,确认没有"期望 X 得到 Y"的解析错误;
  2. 词法改动(加关键字)后确认不与其他关键字前缀冲突;
  3. 控制流/宏改动后确认嵌套场景(循环里套宏、宏里套循环)仍正确。

3.36 一个综合脚本的完整解析

把本章所有机制放进一个脚本,追踪它:

geo 复制代码
SetFactory("OpenCASCADE");
lc = 0.1;
For i In {0:2}
  x = i * 1.0;
  Point(100+i) = {x, 0, 0, lc};       // new* 与显式 tag 混用
EndFor
Macro MakeBox
  Box(b) = {0, 0, 0, 1, 1, 1};
Return
b = 1000; Call MakeBox;
Physical Volume(1) = {b};
Mesh 3;

解析轨迹:

  1. SetFactory("OpenCASCADE"):把 gmsh_yyfactory 置为 "OpenCASCADE"(第 3.1.3 节)。
  2. lc = 0.1;:assignVariable("lc", 0, 普通赋值, 0.1)(第 3.18.2 节)。
  3. For i In {0:2}:fgetpos 记录位置,进入循环(第 3.8.2 节)。
    • 每轮:x = i*1.0 赋值;Point(100+i) = {...} 建点(显式 tag 表达式 100+i);
    • EndFor 时变量自增,未到终点则 fsetpos 回读。
  4. Macro MakeBox ... Return:注册宏(第 3.9.2 节)。
  5. b = 1000; Call MakeBox;:先给外部变量 b 赋值,再 enterFunction 跳入宏体; Box(b) = {...} → OCC_Internals::addBox(b, ...);Return 恢复。
  6. Physical Volume(1) = {b}; → 把体 b 加入物理组(第 3.11.1 节)。
  7. Mesh 3; → 生成三维网格(第 4 章 GenerateMesh)。

注意点 :Point(100+i) 说明 tag 可以是表达式 ------这一点由 FExpr 规则支持;而 new* 只是"表达式的另一种取值方式"。两者统一在表达式体系里,正是第 3.3.3 节设计的威力。

3.37 常见问题 Q&A

Q1:为什么 newp 在词法层而不是语法层处理? 因为这样它就能像普通数值一样参与表达式(p = newp; / Point(newp)),无需特殊语法规则 (第 3.3.3 节)。

Q2:For 循环真的是"重新解析"吗? 是。fgetpos 保存位置、fsetpos 跳回重读(第 3.8.2 节)。代价是重做词法/语法分析,收益是 无需 AST 与执行引擎。

Q3:宏能传参吗? 不能。宏是"无参文本替换 + 位置跳转",参数靠外部变量传递(如 t5.geo 的 x/y/z/r, 第 3.9 节)。

Q4:为什么改 .geo 有时要重新生成 Gmsh.tab.cpp? 只有改 .l/.y(增加 token/规则)才需要。普通脚本改动不需要(第 3.22 节)。

Q5:.pos 文件和 .geo 用同一解析器? 是。PARSER 选项的措辞"required for .geo/.pos scripts"就说明了这点(第 3.29 节)。

Q6:解析器线程安全吗? 不。大量全局状态(第 3.20 节)。同一时刻只能跑一个解析。

Q7:怎么让脚本接受外部参数? 用 GetForced/GetForcedStr(未定义时用默认,第 3.13.4 节),或通过 DefineNumber 配合 Onelab。

Q8:Shape::Type 与 MSH_LIN_2 是一回事吗? 不是。前者是几何类型编码(三位数,100/200/...,第 3.25 节);后者是网格单元编码(两位数, 第 1 章)。

3.38 全章总结

本章讲了 Gmsh 最独特、最被低估的子系统。压缩成几组要点:

语言设计:

  • 语法层级 = 几何层级(Point→Line→Loop→Surface→Volume);
  • (dim, tag) 寻址贯穿;Shape{Type, Num} 是基本单元;
  • 自动编号(new*)下沉到词法层;
  • 维度约束(GeoEntity*)下沉到文法层;
  • 四位一体:声明式几何 + 字符串/列表 + 控制流 + 元编程(宏)。

实现机制:

  • flex + bison,生成物入库(无 flex/bison 也能编译);
  • 全局状态换实现简单(不可重入);
  • For 靠 fgetpos/fsetpos 回读;Macro 靠位置跳转 + FunctionManager 单例;
  • %union 只能放小对象,大命令参数放静态全局(extr 等)。

命令落地:

  • 四条通用原则(双内核分派、可选中缀、同 token 多规则、输入列表复用为输出);
  • gmsh_yyfactory 分派 GEO_Internals/OCC_Internals;
  • 转换器(ListOfShapes2VectorOfPairs 等)跨越解析器容器与内核容器。

与外界的关系:

  • 与 API 一一对应(同一批底层实现);
  • .pos 共用解析器;
  • Python/Julia 走另一条路径(不经解析器);
  • scriptAddCommand 让 C++ 复用脚本入口。

下一章进入网格层,看 GenerateMesh 如何把这些几何数据变成网格。

关键词补充

Shape、GeoDefines.h、MSH_POINT/MSH_SEGM_*/MSH_SURF_*/MSH_VOLUME/MSH_PHYSICAL_*、 LoopOptions、TransfiniteType、TransfiniteArrangement、CircleOptions、AppendOrNot、 HomologyCommand、LP/RP、ListOfListOfDouble2Matrix、MultiStringExprVar、 SurfaceConstraints、yymsg 级别、yyerror。

3.39 完整 token 速查表

Gmsh.l/Gmsh.y 里声明的 token 是 GEO 语言的"词典"。为便于查阅,这里把主要 token 按功能 归类,并注明其语义(多数对应一条脚本命令)。

3.39.1 数学函数 token

token 脚本写法 语义
tExp Exp(x) 指数
tLog/tLog10 Log(x)/Log10(x) 自然/常用对数
tSqrt Sqrt(x) 平方根
tSin/tAsin/tCos/tAcos/tTan/tAtan/tAtan2 Sin(x) ... 三角/反三角
tSinh/tCosh/tTanh Sinh(x) ... 双曲
tFabs/tAbs Fabs(x)/Abs(x) 绝对值
tFloor/tCeil/tRound Floor(x) ... 取整
tMin/tMax Min(a,b)/Max(a,b) 极值
tFmod/tModulo Fmod(a,b) 取模
tHypot Hypot(a,b) √(a²+b²)
tRand Rand() 随机数
tStep Step(x) 阶跃(0/1)

3.39.2 字符串函数 token

token 语义
tPrintf/tSprintf 格式化输出/字符串
tStrCat 连接
tStrPrefix/tStrRelative 前缀/相对路径
tStrReplace/tStrSub 替换/取子串
tStrLen 长度
tStrFind/tFind 查找
tStrCmp 比较
tStrChoice 选择
tUpperCase/tLowerCase/tLowerCaseIn 大小写转换
tAbsolutePath/tDirName 路径处理
tTextAttributes 文本属性

3.39.3 列表函数 token

token 语义
tList 构造列表
tLinSpace 等差
tLogSpace 等比
tListFromFile 从文件读
tCatenary 悬链线(曲线生成)

3.39.4 几何图元 token

token 脚本命令 内核落点
tPoint Point addVertex
tCircle Circle addCircleArc
tEllipse Ellipse addEllipseArc
tCurve Line/Curve addLine/addSpline
tSpline Spline/CatmullRom addSpline
tBSpline BSpline addBSpline
tBezier Bezier addBezier
tNurbs/tNurbsOrder/tNurbsKnots NURBS 参数 addBSpline 参数
tSurface Surface addSurfaceFilling
tPlane Plane Surface addPlaneSurface
tRuled Ruled Surface 规则面
tSphere/tPolarSphere Sphere addSphere
tVolume Volume addVolume
tBox/tCylinder/tCone/tTorus/tEllipsoid/tQuadric/tWedge 基本体 addBox 等(OCC)
tRectangle/tDisk/tWire 矩形/圆盘/线框 addRectangle/addDisk/addWire
tShapeFromFile 从文件读形状 ShapeFromFile

3.39.5 拓扑与关系 token

token 脚本命令
tPhysical Physical(物理组)
tCompound Compound
tPeriodic Periodic
tParent Parent(父子实体)
tUsing Using(扫掠导引)
tTransfinite Transfinite(结构化)
tDegenerated Degenerated(退化)
tRecursive Recursive
tSewing Sewing(缝合)
tThruSections ThruSections(放样)

3.39.6 变换与布尔 token

token 脚本命令
tTranslate/tRotate/tSymmetry/tDilate/tAffine 变换
tExtrude 拉伸(见第 3.10 节)
tBooleanUnion/tBooleanIntersection/tBooleanDifference/tBooleanSection/tBooleanFragments 布尔
tThickSolid 抽壳
tFillet/tChamfer 圆角/倒角
tSplit/tDelete/tCoherence/tHealShapes 分割/删除/缝合/修复
tIntersect/tClosest 求交/最近

3.39.7 网格命令 token

token 脚本命令
tMeshAlgorithm/tMeshSize/tMeshSizeFromBoundary 网格算法/尺寸
tRecombine/tSmoother 重组合/光滑
tTransfinite 结构化
tRefineMesh/tAdaptMesh/tTransformMesh/tRelocateMesh/tReorientMesh/tRecombineMesh 网格变换
tLayers/tScaleLast/tHole 拉伸层
tQuadTriAddVerts/tQuadTriNoNewVerts/tTransfQuadTri/tRecombLaterals 四边形过渡
tSetMaxTag/tSetPartition 标签/分区

3.39.8 变量与元编程 token

token 脚本命令
tDefineNumber/tDefineString/tDefineStruct/tNameStruct/tDimNameSpace/tAppend 定义
tSetNumber/tSetString/tSetTag 赋值
tDefineConstant/tUndefineConstant 常量
tGetValue/tGetStringValue/tGetString/tGetNumber/tGetEnv 取值
tGetForced/tGetForcedStr 强制取值
tFor/tIn/tEndFor/tIf/tElseIf/tElse/tEndIf 控制流
tMacro/tCall/tReturn 宏
tExit/tAbort 退出/中止
tAlias/tAliasWithOptions/tCopyOptions 别名
tShow/tHide/tColor/tColorTable/tDraw 显示
tUnique 唯一性检查

3.39.9 系统与 Onelab token

token 语义
tCpu/tMemory/tTotalMemory/tToday 资源/日期
tGMSH_MAJOR_VERSION/tGMSH_MINOR_VERSION/tGMSH_PATCH_VERSION 版本
tCurrentDirectory/tCurrentFileName/tGmshExecutableName 路径
tOnelabAction/tOnelabRun/tSetChanged/tCodeName Onelab
tSyncModel/tNewModel/tUnsplitWindow 模型/窗口
tSetFactory 内核切换
tMass/tCenterOfMass/tMatrixOfInertia 物理量
tNormal/tCurvature/tDistanceFunction 几何量
tCreateTopology/tCreateGeometry/tClassifySurfaces 拓扑
tHomology/tCohomology/tBetti 计算拓扑
tRenumberMeshNodes/tRenumberMeshElements/tCreateMeshEdges/tCreateMeshFaces 网格拓扑
tExists/tFileExists 存在性
tPlugin 插件
tText2D/tText3D/tInterpolationScheme/tTime/tCombine 后处理
tNameToString/tStringToName 名称转换
tBoundingBox/tFixRelativePath/tSlide/tField 其它

3.39.10 特殊与保留 token

token 语义
tDOUBLE 数值字面量
tSTRING/tBIGSTR 字符串
tEND 语句结束(;)
tAFFECT/tAFFECTPLUS/... 赋值符
tDOTS/tSCOPE :/... / ::
tPi/tMPI_Rank/tMPI_Size/tEuclidean 常量
tEnd 文件结束

3.40 解析器源码导读

给要读/改解析器的人一份文件级导航:

文件 作用 关键点
src/parser/Gmsh.l 词法定义 YY_INPUT、new*、运算符、关键字表
src/parser/Gmsh.y 语法定义 + 辅助函数 %union/%token/%type、规则区、全局变量、treat_Struct_*
src/parser/Gmsh.yy.cpp 生成物:词法分析器 一般不改
src/parser/Gmsh.tab.cpp 生成物:语法分析器 一般不改
src/parser/Parser.h 对外接口 + Symbol/Struct/NameSpaces gmsh_yyparse/gmsh_yysymbols
src/parser/FunctionManager.h / .cpp 宏管理(单例) createFunction/enterFunction/leaveFunction
src/geo/GeoDefines.h Shape + 几何类型编码 三位数编码
src/geo/scriptStringInterface.cpp 字符串 → 解析桥 scriptAddCommand
src/geo/Geo.cpp 脚本 → 内核落地 CreateVertex/CreatePhysicalGroup

读码路径建议 :从一条脚本命令出发(如在 Gmsh.l 搜关键字),到 Gmsh.y 找规则与语义 动作,再到内核 API(第 2 章),最后到实体与网格(第 4 章)。这条路径与第 1 章 1.21.2 节的 "从脚本命令反查"一致。

3.41 完整规则清单的意义

Gmsh.y 的规则区之所以长达数千行,是因为每一条命令都对应一条规则 + 一段语义动作。 这种"命令 → 规则 → 内核调用"的一一映射,带来了两个直接后果:

  1. 语言的表达能力与内核能力严格同步 :内核加一个 addXxx,脚本加一条命令,二者总是一起 演进的。因此读 Gmsh.y 的 token 表就等于读 Gmsh 的功能清单(第 3.4.2 节)。
  2. 维护成本集中在两个文件 :语法与动作都在 Gmsh.y。这既是集中管理的优点(改一处), 也是缺点(文件巨大、并发改易冲突)。

对读者的实操意义:想快速知道"Gmsh 的脚本能做什么",直接读 Gmsh.y 的 %token 区域 ; 想快速知道"某条命令怎么实现",直接搜 tXxx 找规则。

至此,第 3 章的机制、能力、实现与导航已经完整。带着对"脚本如何变成内核调用"的理解,进入 第 4 章的网格层。

3.42 GEO 语言语法摘要(BNF 风格)

把 GEO 语言的核心语法结构整理成 BNF 风格的摘要,便于建立整体印象。注意这是简化摘录 , 真实规则在 Gmsh.y 里有更多变体。

text 复制代码
All           ::= All Statement
Statement     ::= NumericAffectation | StringAffectation | GeoStatement
                | Transform | MeshStatement | ControlStatement
                | Command | ";" | tEND

(* 变量 *)
NumericAffectation ::= Name "=" FExpr tEND
                     | Name "++" tEND
                     | Name "+=" FExpr tEND   (* - = * = / = 同理 *)
StringAffectation  ::= Name "=" StringExpr tEND

(* 表达式 *)
FExpr      ::= tDOUBLE | tSTRING_Constant | "Pi" | "MPI_Rank"
             | Name | FExpr "+" FExpr | FExpr "-" FExpr | FExpr "*" FExpr
             | FExpr "/" FExpr | FExpr "%" FExpr | FExpr "^" FExpr
             | FExpr "<" FExpr | FExpr "<=" FExpr | FExpr "==" FExpr
             | FExpr "&&" FExpr | FExpr "||" FExpr | "!" FExpr
             | FExpr "?" FExpr ":" FExpr
             | tSin "(" FExpr ")" | tMin "(" FExpr "," FExpr ")" | ...
             | "(" FExpr ")" | "-" FExpr
VExpr      ::= FExpr | VExpr "," FExpr            (* 逗号分隔 *)
StringExpr ::= tBIGSTR | tSprintf "(" ... ")" | tStrCat "(" ... ")"
             | ...

(* 几何图元 *)
GeoStatement ::= (tPoint | tCircle | tEllipse | tCurve | tSpline | tBSpline
                | tSurface | tPlane | tVolume | ... )
                 "(" FExpr ")" "=" "{" FExprList "}" tEND

(* 变换 *)
Transform  ::= tTranslate VExpr "{" MultipleShape "}"
             | tRotate    VExpr "{" MultipleShape "}"
             | tSymmetry  VExpr "{" MultipleShape "}"
             | tExtrude   [VExpr] "{" ListOfShapes [ExtrudeParameters] "}"
             | tExtrude   "{" VExpr "," VExpr "," FExpr "}" "{" ListOfShapes "}"
             | ...
MultipleShape ::= "{" ListOfShapes "}"                  (* 参数块 + 形状块 *)

(* 布尔 *)
BoolStatement ::= (tBooleanUnion | tBooleanIntersection | tBooleanDifference
                  | tBooleanFragments | tBooleanSection)
                  "(" FExpr ")" "=" "{" ListOfShapes AppendOrNot "}"
                  "{" ListOfShapes AppendOrNot "}" tEND
AppendOrNot   ::= (* 空 *) | ";" "Delete" ";"

(* 控制流 *)
ControlStatement ::= tFor "(" FExpr tDOTS FExpr [tDOTS FExpr] ")"
                     Statements tEndFor
                   | tFor tSTRING tIn "{" FExpr tDOTS FExpr "}"
                     Statements tEndFor
                   | tIf "(" FExpr ")" Statements
                     { tElseIf "(" FExpr ")" Statements }
                     [ tElse Statements ] tEndIf
Macro            ::= tMacro tSTRING Statements tReturn
Call             ::= tCall StringExpr tEND

(* 列表 *)
ListOfShapes ::= ShapeList
ShapeList    ::= Shape | "{ ShapeList }" | "{ ShapeList, ShapeList }"
Shape        ::= GeoEntity "{" FExprList "}"            (* 带维度约束 *)

读这张摘要的要点:

  1. 语句以 ;(tEND)结尾------这是 GEO 语言的"行结束"约定(第 3.3.2 节)。
  2. GeoEntity "{" FExprList "}" 是形状的统一写法 :Point{1}/Curve{2}/Surface{3}/ Volume{4}。维度由 GeoEntity 约束(第 3.4.4 节)。
  3. MultipleShape 承载"参数块 + 形状块":几乎所有变换命令都是这个结构。
  4. AppendOrNot 是布尔命令的 Delete 子句(第 3.32.3 节)。

3.43 语义动作模式的系统总结

读了大量规则后,可以抽象出语义动作的六种模板。掌握它们,就能快速读懂任何一条命令的 实现。

3.43.1 模板一:直接调用内核

最常见。以建点为例:

c 复制代码
GeoStatement: tPoint '(' FExpr ')' '=' '{' FExpr ',' FExpr ',' FExpr ',' FExpr '}'
  {
    double x=$7, y=$9, z=$11, lc=$13;
    int tag=(int)$3;
    if(gmsh_yyfactory == "OpenCASCADE" && GModel::current()->getOCCInternals())
      GModel::current()->getOCCInternals()->addVertex(tag, x, y, z, lc);
    else
      GModel::current()->getGEOInternals()->addVertex(tag, x, y, z, lc);
  }

特征:解析出参数 → 按 gmsh_yyfactory 分派 → 调内核 API。

3.43.2 模板二:列表 → 内核 → 列表

几何/布尔/拉伸命令的通用模式(第 3.10.1 节):

c 复制代码
List_T *in = $4;
std::vector<std::pair<int,int>> inTags;
ListOfShapes2VectorOfPairs(in, inTags);
... 调内核(inTags → outTags)...
$$ = in; List_Reset($$);
VectorOfPairs2ListOfShapes(outTags, $$);

特征:ListOfShapes2VectorOfPairs 进、VectorOfPairs2ListOfShapes 出、复用输入列表。

3.43.3 模板三:暂存到静态变量

大对象命令(如拉伸、视图)用静态全局暂存:

c 复制代码
tExtrude VExpr '{' ListOfShapes
  { extr.mesh.ExtrudeMesh = extr.mesh.Recombine = false; ... }
                   ExtrudeParameters '}'
  { ... 用 extr ... }

特征:默认值先清零 → 参数块覆盖静态变量 → 最终动作消费它。

3.43.4 模板四:符号表读写

变量/结构体命令:

c 复制代码
Name "=" FExpr  → assignVariable(str, 0, 普通赋值, $3)
Name "+=" FExpr → assignVariable(str, 0, 加赋值, $3)
Field[i].m = v  → treat_Struct_FullName_dot_tSTRING_Float(...)

特征:转调第 3.18.2 节的辅助函数。

3.43.5 模板五:控制流(回读)

For/If(第 3.8 节):记位置 / 跳位置 / 用栈记状态。

特征:没有任何"执行"------全靠词法/语法层的位置操作。

3.43.6 模板六:宏跳转

Macro/Call/Return(第 3.9 节):FunctionManager 的位置栈操作。

特征:同模板五,但用 FunctionManagerStack。

3.43.7 为什么这套模板有效

因为 GEO 语言的设计刻意让命令"形状相似" :都是 Name(params) = {values} 或 Cmd {shapes}。这种一致性让语义动作能收敛到少数模板,也让用户学一门语言就能推及其它命令。

3.44 解析器与几何内核的接口契约

解析器通过一组"约定接口"与内核交互。要接入一个新内核(或理解现有内核),这些接口必须存在:

解析器需要 自研内核提供 OCC 内核提供
建点 addVertex(tag,x,y,z,lc) addVertex(tag,x,y,z,meshSize)
建线 addLine/addSpline/... addLine/addSpline(...)
建环 addCurveLoop addCurveLoop
建面 addPlaneSurface/addSurfaceFilling 同名
建体 addVolume addVolume
拉伸 extrude/revolve/twist extrude/revolve(无 twist)
布尔 (无) booleanXxx
变换 translate/rotate/... _transform
删除 DelXxx 容器 _toRemove
查询最大 tag getMaxTag(dim) getMaxTag(dim)
变更标志 getChanged() getChanged()

契约的关键三条:

  1. tag 引用传递 (第 2.3.2 节):负 tag → 自动分配,写回。这是 new* 的基础。
  2. (dim,tag) 输入输出 :进出的都是 std::vector<std::pair<int,int>>。
  3. getChanged() :内核告知"自上次同步后我变了",触发 GEntity 重建(第 2.29.2 节)。

第 2.2.2 节"实体子类六项作业清单"与本表是同一套契约的两面:前者是"实体如何被查询", 后者是"内核如何被驱动"。

3.45 关于"回读"的性能代价

第 3.8 节说 For 靠回读实现。这里给一个量化的直觉:

  • 每次迭代要重新词法分析 + 语法分析循环体------对一个 20 行的循环体,10 次迭代 = 20 行被 分析 10 次 = 200 行的分析量;
  • 相比"编译成 AST + 求值",回读的代价是 O(迭代数 × 循环体长度) 的分析开销;
  • 但没有 AST 的内存与构建开销,也没有解释执行层的间接调用开销。

权衡结论:对"循环体短、迭代次数少"的典型几何脚本,回读完全可接受;对"大循环 + 复杂循环体",建议把复杂逻辑放进宏(宏只在定义时登记一次位置,调用时跳转,避免重复解析 定义体)。

3.46 从 Gmsh.y 到 API 的自动一致性

一个值得注意的工程现象:GEO 语言的每条命令、公开 API 的每个函数、内核的每个 addXxx, 三者始终保持一一对应 。这种一致性不是自动生成的(API 由 GenApi.py 生成,但脚本规则是 手写的),而是靠开发约定维护:加一条脚本命令时,通常同时加对应的 API 函数(第 10 章)。

因此:

  • 读 Gmsh.y 的 %token → 知道 Gmsh 的能力清单;
  • 读 api/gmsh.h → 知道同一能力的 API 形态;
  • 读内核 addXxx → 知道同一能力的底层实现。

三者互为索引。这是 Gmsh 文档之外最可靠的"功能地图"。

3.47 结语

GEO 语言与解析器是 Gmsh "用户友好性"的技术底座。它用一门精心设计的 DSL,把复杂的几何/网格 操作封装成简洁的命令;用 flex/bison 的实现,把"命令 → 内核调用"的翻译做到可维护;用 回读/跳转的朴素机制,避免了 AST 与执行引擎的复杂度。

它的代价(不可重入、无函数作用域、回读开销、巨型生成文件)是显式选择的结果,换来了 实现简单、行为可预测、能力与内核严格同步。

对读者的最终建议:把 Gmsh.y 当成"Gmsh 的功能目录 + 命令实现索引"。当你不确定 Gmsh 能不能做某件事、或想知道某条命令怎么实现时,先在这里找答案。


下一章进入网格层,看这些几何数据如何被 GenerateMesh 变成一维到三维的网格。

3.48 与解析器相关的选项

脚本/解析行为受若干选项控制(第 1 章选项系统):

  • Geometry.OldNewReg:编号策略(第 3.3.3 节)。默认 1(点一个计数器、其余一个)。
  • General.ScriptingLanguages :脚本语言优先级(GEO/Python/...),决定 .geo 之外的调用。
  • Geometry.OCCTargetUnit /OCC.*:脚本命令落到 OCC 时的单位/行为。
  • General.Terminal:是否把解析器的消息打到终端。
  • General.AbortOnError :错误后是否中止(与 yymsg 配合,第 3.31 节)。
  • Geometry.Tolerance:脚本命令里的容差(如顶点的吸附判定)。

这些选项让"脚本的解析与执行行为"可调,而不必改脚本或代码。

3.49 解析错误的诊断手册

遇到脚本报错时,可按下面的清单定位:

症状 可能原因 排查
"syntax error near ..." 语法错误 检查该行是否缺 ;、括号不匹配、关键字拼写
"GeoEntity dim out of range" 维度越界 检查 Physical/Boundary 等的维度参数
"Unknown field type ..." 尺寸场类型名错 对照第 4 章字段清单
"Twisting extrude not available ..." 用了 OCC 内核做 twist 切回自研内核或用旋转
"Could not extrude shapes" 拉伸输入无效 检查输入形状是否存在/是正确维度
变量未定义 拼写/作用域 用 PrintParserSymbols 或 Print 检查
For 不执行 起止反了 For 区间为空会 skip(第 3.8.2 节)
宏不生效 未 Call 或未设外部变量 宏无参数,需先设 x/y/z/r 等
循环卡住/很慢 循环体过长 把逻辑移入宏(第 3.45 节)

通用技巧 :脚本开头 SetFactory 明确内核;Print 关键变量;用 TestLevel 分环境; 出现错误后看错误计数(非致命错误不中断,第 3.31 节)。

3.50 本章完整导航与延伸

本章 50 个小节按"语言设计 → 实现机制 → 命令落地 → 能力全景 → 实现细节 → 导航"组织。 给不同读者的阅读建议:

  • 想学用 GEO 语言:读 3.1/3.3.3/3.6/3.7/3.8/3.9/3.13/3.17;
  • 想知道命令怎么实现:读 3.10/3.11/3.43;
  • 想改解析器:读 3.2/3.4/3.18/3.20/3.22/3.40;
  • 想知道能力边界:读 3.4.2/3.27/3.39/3.44/3.46。

延伸:本章与第 2 章(内核)、第 4 章(网格)、第 9 章(插件/视图)、第 10 章(API/Onelab) 都有接口。特别是:

  • scriptAddCommand(3.12)→ 第 9 章插件如何复用脚本;
  • Field 规则(3.27.7)→ 第 4 章尺寸场;
  • Plugin 命令(3.13.11)→ 第 9 章插件;
  • OnelabRun(3.13.10)→ 第 10 章 Onelab;
  • Betti/Homology(3.13.8)→ 第 8 章计算拓扑。

把本章当作"在 Gmsh 各子系统之间穿梭的入口",遇到任何一条命令都能顺着它找到对应的实现。

3.51 关键文件与规模速览

文件 规模/性质 说明
Gmsh.l 词法定义(数百行) 关键字表占大部分
Gmsh.y 数千行(全项目最大文件之一) %token + 规则 + 辅助函数
Gmsh.yy.cpp 生成物(很大) flex 输出,勿手改
Gmsh.tab.cpp 生成物(很大) bison 输出,勿手改
Parser.h 数百行 接口 + Symbol/Struct/NameSpaces
FunctionManager.h/.cpp 小 宏管理单例
GeoDefines.h ~47 行 Shape + 几何类型编码
scriptStringInterface.cpp 小 字符串 → 解析桥

一个直观的事实 :Gmsh 的解析器词法 + 语法总共只由两个手写文件(.l/.y)定义, 其余都是生成物或小辅助文件。这让"语言的完整定义"高度集中------想理解 GEO 语言,只需读这两个 文件。

3.52 与第 2 章的接口对照

本章与第 2 章(几何内核)通过明确的接口相连。对照如下:

第 3 章(脚本层) 第 2 章(几何层)
gmsh_yyfactory 决定 GEO_Internals 还是 OCC_Internals
Shape{Type, Num} (dim, tag) 寻址
GeoDefines.h 的几何编码 内核的 addXxx 与 GeomType
语法动作里的 addXxx(...) 内核构造 API
ListOfShapes2VectorOfPairs std::vector<pair<int,int>> 输入输出
Field[i].m = v FieldManager/FieldOption(第 4 章)
Physical ... setPhysicalName / CreatePhysicalGroup
BooleanXxx booleanOperator(OCC)
Extrude extrude/revolve/twist + ExtrudeParams
SetFactory 内核切换(createOCCInternals 等)

这张表说明 :脚本层是几何层的"语法糖 + 调度器"。它不做几何计算,只把命令翻译成内核 调用。理解一条命令,要同时看它的规则(第 3 章)和它的内核实现(第 2 章)。

3.53 一句话总结

如果用一句话概括第 3 章:

Gmsh 用一门手工设计的、语法层级与几何层级一一对应的 DSL,通过 flex/bison 把它翻译成 对两个几何内核的调用;它用"位置回读"实现循环、用"位置跳转"实现宏,以换取解析器的极简 实现,代价是不可重入与有限的循环性能。

这句话里的每一部分,都能在本章找到对应的小节:DSL 设计(3.1/3.25)、flex/bison(3.2)、 双内核分派(3.10/3.44)、回读循环(3.8/3.45)、跳转宏(3.9)、不可重入(3.20)。

带着这套理解,可以进入第 4 章的网格层------那里的每一条网格命令(Mesh/Transfinite/ Recombine/Field),都将在本章建立的"命令 → 内核/选项"框架里被解释。

3.54 本章小节索引

便于快速跳读的一行索引:

  • 3.1 脚本语言全景 :GEO 语言定位、Python/Julia 路径、gmsh_yyfactory。
  • 3.2 flex+bison 管线:生成物入库、三个对外函数、全局状态一览。
  • 3.3 词法 Gmsh.l :YY_INPUT 逐行读、注释/字符串、new* 自动编号、运算符、关键字别名。
  • 3.4 语法 Gmsh.y :%union、token 分组、%type、GeoEntity 维度约束。
  • 3.5 表达式 :FExpr/VExpr、数学函数、两套求值器。
  • 3.6 变量系统 :gmsh_yysymbol、复合赋值、变量命令。
  • 3.7 结构体与命名空间 :Struct/Structs/NameSpaces、treat_Struct_*。
  • 3.8 控制流 :For/If 回读实现、skip/skipTest、代价与收益。
  • 3.9 宏 :FunctionManager 单例、位置跳转。
  • 3.10 Extrude 全解:双内核分派、可选中缀、同 token 多规则、列表复用。
  • 3.11 其它命令落地:Physical、布尔、维度约束、列表转换器。
  • 3.12 脚本桥 :scriptStringInterface。
  • 3.13 能力全景:数学/字符串/列表/文件/系统/可视化/物理量/拓扑/网格/Onelab/视图。
  • 3.14 错误与调试 :yymsg 级别、PrintParserSymbols、调试技巧。
  • 3.15 GEO vs Python:两语言对照。
  • 3.16 性能与最佳实践:6 条。
  • 3.17 常见陷阱:6 条。
  • 3.18 实现细节 :Gmsh.y 结构、变量函数、表达式规则、结构体函数。
  • 3.19 内存管理 :List_T、strsave、MallocUtils。
  • 3.20 并发与重入:不可重入、临时文件隔离。
  • 3.21 完整轨迹 :t5.geo 的 CheeseHole。
  • 3.22 扩展语言:加 token/规则的成本。
  • 3.23 与其它 DSL 对比。
  • 3.24 本章小结。
  • 3.25 Shape 与 GeoDefines :type/100 = 维度+1。
  • 3.26 词法细节 :yytext/yylval、数字识别、关键字组织。
  • 3.27 规则分类:按命令族。
  • 3.28/3.29 Field/视图脚本的解析轨迹。
  • 3.30 bison 优先级。
  • 3.31 错误恢复。
  • 3.32 子规则细节 :LoopOptions/AppendOrNot/...
  • 3.33 历史关系(Tcl/Lua)。
  • 3.34 两条路径对照(脚本 vs API)。
  • 3.35 测试。
  • 3.36 综合脚本解析。
  • 3.37 Q&A。
  • 3.38 全章总结。
  • 3.39 token 速查表(10 组)。
  • 3.40 源码导读。
  • 3.41/3.42 BNF 摘要。
  • 3.43 语义动作六模板。
  • 3.44 接口契约。
  • 3.45 回读性能。
  • 3.46 脚本与 API 一致性。
  • 3.47 结语。
  • 3.48-3.54 选项/诊断/导航/规模/对照/总结/索引。

(本章完,共 54 个小节。)

第 3 章回顾:GEO 语言是 Gmsh 的"用户接口语言"。它的语法层级与几何层级严格对应, 用 Shape{Type, Num} 承载 (dim, tag) 寻址;自动编号(new*)下沉到词法层、维度约束 (GeoEntity*)下沉到文法层;For/Macro 分别用"位置回读"与"位置跳转"实现,换取解析器 的极简;gmsh_yyfactory 在语法动作里分派双内核。它既是 Gmsh 的能力清单,也是能力与内核 严格同步的保证。

本章为后续章节提供的"坐标"总结如下:任何一条网格命令(第 4 章),都能在本章找到它的语法 规则;任何一条后处理命令(第 9 章),都能找到 View/Plugin 的规则;任何 Onelab 交互 (第 10 章),都能找到 OnelabRun 的规则。第 3 章是"命令层"的总目录,第 4-10 章是各 子系统的实现层------两者通过本章的"语义动作模板"(第 3.43 节)与"内核接口契约" (第 3.44 节)精确对接。

(本章完。)

3.55 构建视角:解析器为何"生成物入库"

前面反复提到"生成物入库"。这里从构建系统与词法文件本体,把这件事讲透------它是理解 Gmsh 解析器工程取舍的最后一环。

3.55.1 只编译生成物,不调用 flex/bison

src/parser/CMakeLists.txt 全文只有十几行,核心是:

cmake 复制代码
set(SRC
  Gmsh.tab.cpp
  Gmsh.yy.cpp
  FunctionManager.cpp
)

file(GLOB HDR RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.h)
append_gmsh_src(src/parser "${SRC};${HDR}")

关键结论:构建系统从不运行 flex/bison。 Gmsh.l/Gmsh.y 只是"源定义",真正参与编译的 是已经生成好的 Gmsh.tab.cpp(bison 输出:语法分析器 + 内嵌的语义动作)与 Gmsh.yy.cpp (flex 输出:词法分析器),再加上人手写的 FunctionManager.cpp。于是:

  • 普通用户 拿到发行版源码即可 cmake && make,不需要 bison/flex;
  • 改语言的开发者 才需要在改 .l/.y 后手动重新生成那两个大文件并提交。
  • 目录里同时存在 src/parser/Gmsh.tab.hpp(bison 的 C++ 头,供 .l/.y 与外部共享 token 编号)。

这与第 1 章 CMake 选项 PARSER("Enable GEO file parser")的关系是:该选项决定是否编译 src/parser 这个目录 (即是否把这个子系统链进 gmsh)。关掉它,.geo/.pos 脚本就无法解析 ------这正是第 3.29 节".pos 与 .geo 共用同一解析器"的另一面。

3.55.2 生成物入库带来的"兼容补丁"

因为编译的是某个历史版本 flex/bison 生成的产物 ,而编译器却在不断升级,词法文件头部就 必须打补丁。src/parser/Gmsh.l 第 44--46 行:

c 复制代码
// undefine register for C++11 compatibility of files generated with old
// versions of flex/bison
#define register

以及第 39--42 行针对 Windows:

c 复制代码
#if defined(WIN32)
#define isatty(arg) -1
#define YY_NO_UNISTD_H
#endif

两点说明:

  1. 老版本 flex/bison 生成的代码会使用 register 关键字(C++17 已移除),Gmsh 在词法头部把 它定义成空,从而让旧生成物能在新编译器下编译;
  2. Windows 上没有 POSIX 的 isatty/unistd.h,用宏屏蔽。

这是"生成物入库"策略的直接代价:源码要兼容"曾经的生成器版本",于是要在词法层做语言/ 平台兼容 shim。相比之下,若构建时现生成,就不需要这些补丁(生成的代码会随生成器版本同步)。

3.55.3 逐行读取的 YY_INPUT

Gmsh.l 用自定义 YY_INPUT(第 24--37 行):

c 复制代码
#define YY_INPUT(buf,result,max_size)					\
  {									\
    int c = '*', n;							\
    for(n = 0; n < (int) max_size && (c = fgetc(yyin)) != EOF &&        \
           c != '\n' && c != '\r'; ++n)                                 \
      buf[n] = (char) c;                                                \
    if(c == '\n' || c == '\r') {                                        \
      buf[n++] = (char) c;                                              \
      if(c == '\n') yylineno++;                                         \
    }                                                                   \
    if(c == EOF && ferror(yyin))                                        \
      Msg::Error("Input in flex scanner failed");			\
    result = n;                                                         \
  }

它逐行 喂给 flex(读到行尾就停),顺便维护 yylineno。为什么要逐行(而不是整块读)? 因为 For 回读(第 3.8 节)与 Macro 跳转(第 3.9 节)依赖 fgetpos/fsetpos 能定位到 行首------这是"回读式控制流"对 IO 层提出的硬要求。若用大缓冲一次性读入,位置定位的语义就 不可靠(缓冲区与文件位置的对应关系会变复杂)。

3.55.4 词法层声明的四个辅助函数

.l 头部(第 19--22 行)声明了四个由其它文件实现的辅助函数:

c 复制代码
void   parsestring(char endchar);
char  *strsave(char *ptr);
void   skipcomments(void);
void   skipline(void);
  • parsestring(endchar):读到指定结束符(" 或 '),把字符串交给词法层(配合返回值 tBIGSTR,见第 59--64 行的两条规则);
  • strsave(ptr):复制字符串,避免 yytext 缓冲被覆盖(第 3.19.2 节);
  • skipcomments()/skipline():处理 /* ... */ 与 // ... 两种注释(第 61--62 行的规则)。

注意 parsestring 的实现不在 .l 里,而在 .y 的尾部函数区 :词法文件只声明、语法文件 实现。这种"声明与实现分居两个文件"的做法,是 Gmsh 解析器的一个实现特征------因为两者最终会被 编译进同一个目标(Gmsh.tab.cpp 与 Gmsh.yy.cpp 链接到一起),跨文件的符号可以互相解析。

3.55.5 对读者的意义

把这条链路浓缩成阅读指南:

你想做的事 该读/改哪里
读语言定义(有哪些命令/关键字) Gmsh.l(词法关键字表)+ Gmsh.y(%token 与规则)
理解构建如何编译解析器 src/parser/CMakeLists.txt(只编译生成物)
改语言(加命令/关键字) 改 .l/.y → 用 flex/bison 重新生成 Gmsh.yy.cpp/Gmsh.tab.cpp
只改命令语义而不改语法 直接改 Gmsh.tab.cpp 里的语义动作为权宜之计(不推荐)

最后一条最重要 :由于构建编译的是生成物 ,如果你只改了 Gmsh.y 却忘了重新生成 Gmsh.tab.cpp,改动完全不会生效------编译的仍是旧生成物。这就是第 3.22 节"改语言成本高" 的根因:成本不在写规则,而在"改规则 → 重新生成 → 提交两个巨大的生成文件"。

理解"生成物入库 + 构建不调用生成器 + 词法头部兼容补丁 + 逐行 YY_INPUT 支撑回读"这四点, 才算真正读懂了第 3 章最后一块拼图:Gmsh 用工程上的确定性 (生成物固定、构建简单、行为可 复现)换取了开发上的集中性(语言定义只在两个手写文件里)。这与第 1 章"代码规模与构建 选项"的取舍、第 4 章"随机种子固定以保可复现"是同一种工程哲学的延续。

相关推荐
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-16 工程与工具链:Windows 交叉编译、性能剖析、数据交换与测试
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 5 章 二维曲面网格算法
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 1 章 架构总览与数据模型
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-12 `coded` 系列:在字典里写 C++ 并在运行期编译
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-01 多相流与 VOF:`interFoam`、界面压缩与 MULES
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-14 `functionObjects`、求解器控制框架 与 表达式系统
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-13 `fvOptions` / `fvConstraints` / `corrections`
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-03 拉格朗日粒子与喷雾:`Cloud`/`parcel` 模板体系、运动积分与子模型群
算法·开源
hetao17338371 小时前
校内场-提高组 ZYZSC-S-Round 2
c++·算法