DSL(ドメイン固有言語)は、特定のドメインを対象とした言語です。その価値は、通常の API をより短いチェーン呼び出しに変更するだけでなく、ドメインの概念、制約、組み合わせ方を表現の中に直接現れさせる点にあります。
言語を設計する前に、典型的なタスクとエラーを収集します。ユーザーは何を表現したいのか、どの組み合わせを拒否すべきか、ルールはどのくらいの頻度で変化するか、誰がそれを保守するのか、失敗をどのように特定するのか。構文はこれらのニーズに奉仕すべきです。
セマンティクスを先に設計する:価格ルールを例として
ルール order.total >= 100 USD を考えます。中括弧かインデントかを選ぶ前に、total が税込み金額か税抜き金額か、欠落している場合はエラーかゼロか、通貨が異なる場合に比較可能か、数値が整数のセント単位か10進固定小数点数かという説明が必要です。
これらが定義されていない場合、パーサーが入力を正しく受け付けていても、システムは安定した回答を提供できません。DSL の「ドメイン固有」という性質は、まず型、制約、実行ルールに現れます。
ルール DSL: 構文解析後にドメインチェックが始まる
入力ルール
order.total >= 100 USD
1
構文構造を解析し、ソースコードの位置を記録
比較式: フィールド、演算子、金額リテラル
2
名前を解析し、ドメインの型をチェック
total が存在するか、金額の通貨が互換性があるか
3
安定したルール表現を生成
欠落値、丸め、エラーを明示的に処理
4
解釈またはコンパイルして実行
予算、診断、結果追跡の一貫性を保つ
5
構文が合法かどうかと、ルールにドメイン的な意味があるかどうかは、独立した2つのチェックです。
同じルールでも、偽、真、入力エラーの3つの結果がある
order.total >= 100.00 USD
閾値 10000 セント、通貨 USD にコンパイル
9999 セント USD → false
正常な条件不充足
10000 セント USD → true
境界値が ≥ を満たす
欠落 / EUR → error
暗黙的にゼロに変更したり、自動為替交換したりしない
ドメイン契約は、パーサー、ビジュアルエディタ、実行バックエンドのすべてで同一でなければならず、構文の外観によって決まるものではありません。
このルールを最小限の実行可能言語として実装する
この例では「フィールド >= 非負の金額 通貨」のみをサポートします。ドメインモデルは、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-z 0-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、優先順位を追加する場合は、正規表現を貼り付けて構造を隠蔽するのではなく、明確な構文とパーサーを設計する必要があります。fullmatch は、ルールの後に無視される末尾テキストがないことを保証します。
金額の解析は、10進文字列から直接整数のセントに変換され、0.1 は 10 セントとして解釈され、0.001 は拒否され、こっそりと丸められません。実行時には type(amount) is int を使用します。これは、Python の bool が int のサブクラスであり、このドメインでは true を1セントとして受け付けないためです。ここでルールセマンティクスは例によって明確に定義されており、ホスト言語の暗黙的な型変換は借用しません。
診断精度と言語能力を同時に拡張する
現在のフィールドまたは通貨のエラーは一致範囲を保持していますが、構文エラーは全体としてのヒントしかありません。これは最小限の実行エンジンであり、完全なエディタ診断ではありません。対話型編集を対象とする場合、トークンとノードの両方に span を保持し、「>= の後に金額がない」「通貨がサポートされていない」「ルールが未完成」などを区別し、診断をソースコードにマッピングできます。
入力 どの層で処理すべきか なぜすべて false を返してはいけないか
合計額 99.99 USD 実行して false を取得 有効なデータだが、ルールに一致しない
order.totla 名前チェックで拒否 綴りミスによりルールが永遠に発動しない
0.001 USD 現在の金額構文で拒否 精度ポリシーではこの入力をまだ許可していない
合計額が欠落 実行時の入力エラー データ不足と閾値未満は意味が異なる
合計額の通貨が EUR ドメインの互換性エラー 暗黙的な為替変換ルールは存在しない
ビジネスで欠落値を unknown に変え、and/or で三値論理を使用したい場合は、それを別の明示的な言語セマンティクスとして扱うべきです。単にバックエンドですべての例外をキャッチして false を返すだけでは、2つのバックエンドが同じルールで異なる回答を生成することになります。
パースは成功したのに、なぜすべての失敗を false として扱えないのか?
編集可能なルールは、4つの「成功/失敗」ボタンよりも、言語の境界をよりよく検証できます。以下の入力は、完全な一致、フィールドチェック、通貨チェック、実行時入力チェックを順に通過し、制限された CompareAmount 表現を生成します。綴りを修正し、3桁の小数を書き、合計額を欠落させると、異なるエラーカテゴリに属するはずです。合法な金額が閾値を下回る場合にのみ 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":"注文合計額 / セント","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 の遅延評価とエイリアシング
マクロはまた、ホスト言語内に個別に解析が必要なトークン構文を埋め込むこともできます。したがって、「内部 DSL には必ずしも独自のパーサーがない」というのも一般則ではありません。そのセマンティクスチェックがマクロフェーズで行われるのか、後続の型チェックフェーズで行われるのかは、インターフェース設計によります。
外部 DSL: 独自の入力言語を持つ
独立した構文は、ホスト言語を跨いで使用したい場合、ドメインユーザー向け、または表現能力を厳密に制限したい場合に適しています。独自の字句解析、構文解析、位置情報、診断、バージョン処理が必要ですが、マシンコードを生成する必要はありません。
一般的なバックエンドには、AST の直接解釈、ルール IR への lowered、SQL やホストソースコードの生成、バイトコードの生成、マシンコードへのコンパイルなどがあります。選択は、実行頻度、デプロイ、デバッグ、移植性、制限能力によって決まり、「速くしたいからコンパイル、デバッグしたいからトランスパイル」という単純な話ではありません。
ある設定は、読み込み時にチェックされた後、繰り返し解釈されるだけでパフォーマンス要件を満たすことができます。計算集約型の DSL では、IR 最適化と専用バックエンドへの投資が価値があるかもしれません。両方とも完全で合理的な設計です。
フロントエンドチェックを階層化する
階層 例題 適切な診断
字句 金額リテラルに不正な文字が含まれている 具体的な文字と許可フォーマットを指摘
構文 比較演算に右辺値がない 欠落項目と現在位置を指摘
名前解決 order.totla が存在しない未知のフィールドを指摘し、候補の綴りを提示
型と単位 金額と文字列の比較 両側の型と許可された操作を説明
ドメイン制約 現在のルールでは個人機密フィールドへのアクセスが許可されていない 制限とルールコンテキストを説明
実行 データの欠落または予算の枯渇 ソースコードに関連付け可能な制御されたエラーを返す
ノードはソース位置を保持し、IR に lowered する際も必要なマッピングを保持すべきです。これにより、生成されたコードやランタイムエラーが、内部の一時変数を表示するのではなく、ユーザーが書いたルールに戻ることができます。
型、効果、リソースの境界
言語の表現能力を制限することで、検証が容易になります。例えば、任意のネットワークやファイル呼び出しを禁止し、明確に定義された純粋関数のみを提供する。ループを制限するか、有界集合操作を使用する。実行にステップ数、時間、メモリ予算を設定する。
しかし、「DSL である」ことは、すでに安全に隔離されていることを意味しません。ホスト実行への埋め込み、マクロ展開、外部関数の呼び出し時には、その権限と副作用を明確にする必要があります。ルールソースが信頼できない場合、文字列を連結してホストの eval に渡すだけではサンドボックスと呼ぶことはできません。
SQL を生成する DSL の場合、値はバインドパラメータを通じて渡すべきです。識別子は、構文、引用、許可された集合に応じて処理されます。パラメータ化された値が任意のテーブル名や列名を直接置き換えることはできません。
中間表現により複数のエントリがセマンティクスを共有する
ビジュアルルールエディタ、ホスト API、外部テキストパーサーのすべてが、同じドメイン IR を生成できるようにできます。型チェック、最適化、実行はこの表現を共有し、3つのエントリが異なるセマンティクスを生成しないようにします。
例えば、IR ノード CompareAmount(field, amount, currency) は、未解析の文字列断片よりもチェック、フォーマット、シリアライズが容易です。IR をエクスポートする場合は、バージョンを定義すべきです。内部メモリ構造を長期互換プロトコルとして直接使用しないでください。
バージョンの進化: ルールも長期的なデータ
言語がリリースされた後、フィールドの削除、デフォルト値の変更、丸めルールの変更、キーワードの追加などが、古いルールの意味を変更する可能性があります。ルールに言語バージョンを記録し、解析とマイグレーション戦略を提供し、正規化された表現と再現可能なテストケースを保持します。
後方互換性は、「古いテキストが依然として解析できる」かどうかだけでなくチェックすべきではありません。古いルールを解析した後に異なる結果が得られる場合も、セマンティクスの破壊です。回帰テストケースは、境界値、欠落値、エラー入力をカバーし、インタープリタとコンパイルバックエンドの結果を比較する必要があります。
マイグレーション前に結果とエラーカテゴリを比較する
旧バージョンではデフォルトで欠落した金額がゼロだったが、新バージョンではエラーを報告するようになったと仮定します。古いテキストは依然として解析できますが、動作が変更されています。マイグレーションケースは、AST が同じように見えるかどうかだけでなく、入力データ、ルールバージョン、期待される結果またはエラーカテゴリを同時に保存すべきです。
同じケースセットを、インタープリタ、SQL を生成するバックエンド、その他のコンパイルバックエンドに渡すことができます。正常な結果は一致し、欠落、通貨不一致、精度の境界の処理も一致する必要があります。正規化フォーマットはスペースや金額の表示を変更できますが、0.1 の精度ポリシーや >= の境界をこっそりと変更することはできません。
選択のクイックリファレンス
要件 検討すべき起点
開発者が同じ言語内で使用し、通常の API で表現しやすい 関数、型安全なビルダー、または演算子インターフェース
ホストコードの繰り返し生成が必要で、構文構造を読む必要がある マクロまたはコードジェネレーター
言語を跨いで配布、ドメインユーザーが編集、独立したバージョン管理 外部構文と安定したセマンティクスモデル
複数のエントリがルール能力を共有する 統一されたドメイン IR に、異なるフロントエンドを接続
内部 DSL が最終的に外部言語に進化する必要はありません。外部言語も「より高度」な終点ではありません。まず最小限の表現力で実際のタスクをカバーし、その後、明確なニーズに基づいて拡張します。
参考
Keywords: DSL, 埋め込み言語、フローエント API、式テンプレート、ドメインセマンティクス、インタープリタ、コード生成、言語進化