DSL 是面向特定领域的语言。它的价值是让领域概念、约束和组合方式直接出现在表达中,而不只是把普通 API 改成更短的链式调用。
设计语言前,先收集典型任务和错误:使用者要表达什么,哪些组合必须拒绝,规则多久变化,谁维护它,失败如何定位。语法应服务这些需求。
先设计语义:以价格规则为例
考虑规则 order.total >= 100 USD。在选择大括号还是缩进之前,需要说明 total 是含税金额还是税前金额,缺失时是错误还是零,币种不同能否比较,数值采用整数分还是十进制定点数。
若这些没有定义,parser 即使正确接受输入,系统仍然无法给出稳定答案。DSL 的“领域专用”首先体现在类型、约束和执行规则上。
将这条规则落实为一个最小可执行语言
本例只支持“字段 >= 非负金额 币种”。领域模型规定 order.total 是以分计的、已确定的订单总额;只接受 USD,不做汇率转换,缺失总额报告错误。金额最多 9 位整数和 2 位小数,避免把任意长数字与不明确的舍入规则留给执行器。
from dataclasses import dataclass
import re
@dataclass(frozen=True)
class Rule:
field: str
cents: int
currency: str
span: tuple[int, int]
class RuleError(ValueError):
pass
PATTERN = re.compile(
r"(?P<field>[a-z][a-z0-9_.]*)\s*>=\s*"
r"(?P<whole>[0-9]{1,9})(?:\.(?P<fraction>[0-9]{1,2}))?"
r"\s+(?P<currency>[A-Z]{3})"
)
def compile_rule(source):
if len(source) > 256:
raise RuleError("rule too long")
match = PATTERN.fullmatch(source)
if match is None:
raise RuleError("expected FIELD >= AMOUNT CURRENCY")
field = match["field"]
if field != "order.total":
raise RuleError(f"unknown field at {match.span('field')}: {field}")
if match["currency"] != "USD":
raise RuleError(f"unsupported currency at {match.span('currency')}")
fraction = (match["fraction"] or "").ljust(2, "0")
cents = int(match["whole"]) * 100 + int(fraction)
return Rule(field, cents, "USD", match.span())
def evaluate(rule, order):
if "total_cents" not in order or order["total_cents"] is None:
raise RuleError("missing order.total")
amount = order["total_cents"]
if type(amount) is not int or amount < 0:
raise RuleError("total must be nonnegative integer cents")
if order.get("currency") != rule.currency:
raise RuleError("currency mismatch")
return amount >= rule.cents
rule = compile_rule("order.total >= 100.00 USD")
assert rule.cents == 10000
assert not evaluate(rule, {"total_cents": 9999, "currency": "USD"})
assert evaluate(rule, {"total_cents": 10000, "currency": "USD"})
assert compile_rule("order.total >= 0.1 USD").cents == 10
for source in ["order.totla >= 100 USD", "order.total >= 1 EUR",
"order.total >= 0.001 USD", "order.total >= 1 USD trailing"]:
try:
compile_rule(source)
except RuleError:
pass
else:
raise AssertionError("invalid rule accepted")
for order in [{}, {"total_cents": None},
{"total_cents": True, "currency": "USD"},
{"total_cents": 10000, "currency": "EUR"}]:
try:
evaluate(rule, order)
except RuleError:
pass
else:
raise AssertionError("invalid input accepted")
这个受限语言没有嵌套表达式,所以完整匹配一个规则的正则足以表达当前语法;加入括号、and/or 与优先级后,应设计明确语法和 parser,不能不断拼接正则来掩盖结构。fullmatch 也确保规则后没有被忽略的尾部文本。
金额解析直接从十进制字符得到整数分,0.1 解释为 10 分,0.001 被拒绝而不偷偷舍入。执行时使用 type(amount) is int,是因为 Python 的 bool 是 int 的子类,而本领域不接受 true 当作一分钱。这里的规则语义由例子明确定义,不借用宿主语言的隐式类型转换。
诊断精度与语言能力一起扩展
当前字段或币种错误保留了匹配区间,但语法错误只有整体提示;它是最小执行器,不是完整编辑器诊断。面向交互编辑时,可以将 token 与节点都保留 span,区分“>= 后缺金额”“币种不支持”“规则未写完”,再把诊断映射回源码。
| 输入 | 应在哪层处理 | 为什么不能都返回 false |
| 总额 99.99 USD | 执行得到 false | 有效数据,规则不匹配 |
| order.totla | 名字检查拒绝 | 拼写错误会让规则永远不生效 |
| 0.001 USD | 当前金额语法拒绝 | 精度政策尚不允许这种输入 |
| 总额缺失 | 执行输入错误 | 缺数据与低于门槛含义不同 |
| 总额币种 EUR | 领域兼容性错误 | 不存在隐式换汇规则 |
如果业务希望缺失值变成 unknown,并让 and/or 使用三值逻辑,就应把它作为另一项明确语言语义。不能仅在某个后端 catch 所有异常返回 false,否则两个后端会在相同规则上产生不同答案。
解析成功,为什么还不能把所有失败都当成 false?
一个可编辑的规则比四个“成功/失败”按钮更能检验语言边界。下面输入会实际经过完整匹配、字段检查、币种检查和执行输入检查,再生成受限的 CompareAmount 表示。修改拼写、写入三位小数、让总额缺失,应该落在不同错误类别;只有合法金额低于门槛时才返回 false。
正在呈现知识画面
解析成功,为什么还不能把所有失败都当成 false?语法、字段、单位和运行时数据是不同契约。99.99 未达门槛是 false,未知字段或缺失数据是错误;金额直接转整数分,不经浮点舍入。
{"id":"dsl-domain-contract","title":"解析成功,为什么还不能把所有失败都当成 false?","summary":"语法、字段、单位和运行时数据是不同契约。99.99 未达门槛是 false,未知字段或缺失数据是错误;金额直接转整数分,不经浮点舍入。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"form-field\"><span data-i18n=\"rule\"></span><input type=\"text\" id=\"rule\" value=\"order.total >= 100 USD\" maxlength=\"70\" autocomplete=\"off\" spellcheck=\"false\"></label><div class=\"control\"><label for=\"amount\" data-i18n=\"amount\"></label><output for=\"amount\" id=\"amount-value\"></output><input id=\"amount\" type=\"range\" min=\"9900\" max=\"10100\" value=\"9999\" step=\"1\"></div><label class=\"form-field\"><span data-i18n=\"currency\"></span><select id=\"currency\"><option>USD</option><option>EUR</option></select></label><label class=\"check\"><input type=\"checkbox\" id=\"missing\"><span data-i18n=\"missing\"></span></label><div id=\"gates\" class=\"tile-grid\"></div><section class=\"panel\"><h3 data-i18n=\"result\"></h3><div id=\"result\"></div></section><p id=\"ir\" class=\"source\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#result{font:17px/1.8 var(--viz-font-ui);min-height:3.6em;overflow-wrap:anywhere}#ir{min-height:5.4em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function draw(){const text=$('#rule').value,match=/^\\s*([A-Za-z_][A-Za-z0-9_.]*)\\s*>=\\s*([0-9]{1,9})(?:\\.([0-9]{1,2}))?\\s+([A-Z]{3})\\s*$/.exec(text);let state=0,result='SyntaxError',ir='∅';if(match){state=1;if(match[1]!=='order.total')result='NameError: '+match[1];else{state=2;if(match[4]!=='USD')result='UnitError: '+match[4];else{state=3;const cents=Number(match[2])*100+Number((match[3]||'').padEnd(2,'0'));ir='CompareAmount(order.total, '+cents+', USD)';if($('#missing').checked)result='MissingValue';else if($('#currency').value!=='USD')result='CurrencyMismatch';else{state=4;result=String(+$('#amount').value>=cents)}}}}set('amount-value',$('#amount').value);cells('gates',['syntax','field','unit','data'].map((k,i)=>({text:t(k)+' '+(i<state?'✓':i===state?'✗':'—'),cls:i<state?'on':i===state?'gold':'empty'})));set('result',result);set('ir',ir);$('#result').dataset.stage=state;}document.querySelectorAll('input').forEach(e=>e.oninput=draw);$('#currency').onchange=draw;draw();","audio":false,"strings":{"amount":"订单总额 / cent","currency":"订单币种","data":"执行输入","field":"字段","heading":"解析成功,为什么还不能把所有失败都当成 false?","intro":"编辑金额规则,再改变订单总额、币种或缺失状态。每层给出具体结果,只有合法数据才进入比较。","missing":"订单总额缺失","reset":"重置","result":"结果或错误类别","rule":"规则文本","scope":"只接受 field >= 非负金额 currency,最多9位整数和2位小数;字段仅 order.total、币种仅USD,金额以整数分保存。无嵌套、汇率或外部执行。","scopeLabel":"模型范围与约定","syntax":"语法","unit":"币种"}}
内部 DSL:借用宿主语法与工具
内部或嵌入式 DSL 利用函数、运算符、类型、闭包等宿主机制表示领域操作。例如查询构造器可以把字段比较组合为一个表达式树,最后由后端生成参数化 SQL。
宿主语言的类型检查是基础能力,但领域检查不是免费的。只有把字段类型、操作约束和状态转换编码进 API,编译器才能帮助拒绝相应误用;直接接收任意字符串的 where(string) 不会自动获得字段名检查或 SQL 安全性。
构造器既可以立即执行,也可以保存中间表示后解释或编译。方法链只是表面语法,不能从链式写法推断它在构建 AST。
表达式模板:延迟求值也需要正确的代价模型
矩阵库可以用表达式模板保留 B + C * D 的结构,选择循环融合或优化求值顺序。但矩阵乘法、别名和复用可能使临时结果更正确或更高效;不能宣称所有表达式都不分配临时矩阵或自动得到最优代码。Eigen 延迟求值与别名
宏还可以在宿主语言内嵌入需要单独解析的 token 语法。因此“内部 DSL 必然没有自己的 parser”也不是通则。其语义检查发生在宏阶段还是后续类型检查阶段,要看接口设计。
外部 DSL:拥有自己的输入语言
独立语法适合希望跨宿主语言使用、面向领域用户或严格限制表达能力的场景。它需要自己处理词法、语法、位置、诊断和版本,但不必生成机器码。
常见后端包括直接解释 AST、降低到规则 IR、生成 SQL 或宿主源码、生成字节码,也可以编译到机器码。选择取决于执行频率、部署、调试、可移植性与限制能力,而非简单的“想快就编译、想调试就转译”。
一段配置可以在加载时检查后反复解释,已经满足性能要求;一个计算密集 DSL 则可能值得投入 IR 优化与专用后端。两者都属于完整而合理的设计。
把前端检查分层
| 层次 | 示例问题 | 合适的诊断 |
| 词法 | 金额字面量中出现非法字符 | 指出具体字符与允许格式 |
| 语法 | 比较运算缺少右侧值 | 指出缺失项和当前位置 |
| 名字解析 | order.totla 不存在 | 指出未知字段,可给候选拼写 |
| 类型与单位 | 金额和字符串比较 | 说明两侧类型与允许操作 |
| 领域约束 | 当前规则不允许访问个人敏感字段 | 说明限制及规则上下文 |
| 执行 | 数据缺失或预算耗尽 | 返回可关联源码的受控错误 |
节点应保留源位置,降低为 IR 时也应保留必要映射。这样生成代码或运行时错误可以回到使用者写的规则,而不是只显示内部临时变量。
类型、效果与资源边界
限制语言表达能力可以使验证更容易。例如禁止任意网络和文件调用,只提供明确定义的纯函数;限制循环或使用有界集合操作;为执行设置步数、时间和内存预算。
但“是 DSL”不等于已经安全隔离。嵌入宿主执行、宏展开或调用外部函数时,仍需明确其权限与副作用。若规则来源不受信任,不能仅把字符串拼接后交给宿主 eval 就称为沙箱。
对于生成 SQL 的 DSL,值应通过绑定参数传递;标识符则按语法、引用与允许集合处理。参数化值不能直接代替任意表名或列名。
中间表示让多种入口共享语义
可以让一个可视化规则编辑器、一个宿主 API 和一个外部文本 parser 都生成同一份领域 IR。类型检查、优化与执行共享该表示,避免三种入口产生不同语义。
例如 IR 节点 CompareAmount(field, amount, currency) 比未解析的字符串片段更容易检查、格式化和序列化。需要导出 IR 时还应定义版本,不要把内部内存结构直接当成长期兼容协议。
版本演进:规则也是长期数据
语言上线后,字段删除、默认值变化、舍入规则调整或关键字新增都可能改变旧规则意义。可以为规则记录语言版本,提供解析与迁移策略,并保留规范化表示和可复现的测试样例。
向后兼容不能只检查“旧文本仍能解析”。旧规则解析后若得到不同结果,同样是语义破坏。回归样例应覆盖边界值、缺失值和错误输入,并比较解释器与编译后端的结果。
迁移前比较结果与错误类别
假设旧版默认缺失金额为零,新版改为报告错误,旧文本仍能解析,却已经改变行为。迁移样例应同时保存输入数据、规则版本、预期结果或错误类别,而不只比较 AST 是否长得相同。
同一组案例可以交给解释器、生成 SQL 的后端与其他编译后端。正常结果要一致,缺失、币种不匹配、精度边界的处理也要一致。规范化格式可以改变空格或金额显示,但不能把 0.1 的精度政策或 >= 的边界悄悄改掉。
选型速查
| 需求 | 可考虑的起点 |
| 开发者在同一语言内使用,普通 API 易表达 | 函数、类型安全构造器或运算符接口 |
| 重复生成宿主代码,需要读取语法结构 | 宏或代码生成器 |
| 跨语言分发、领域用户编辑、独立版本管理 | 外部语法与稳定语义模型 |
| 多个入口共享规则能力 | 统一领域 IR,再接不同前端 |
内部 DSL 不必最终进化为外部语言,外部语言也不是“更高级”的终点。先用最小表达能力覆盖真实任务,再依据明确需求扩展。
参考
Keywords: DSL, embedded language, fluent API, expression templates, domain semantics, interpreter, code generation, language evolution