A Domain-Specific Language (DSL) is a language tailored to a specific domain. Its value lies in allowing domain concepts, constraints, and composition methods to appear directly in the expression, rather than simply turning a standard API into a shorter chain of method calls.
Before designing the language, collect typical tasks and errors: What do users want to express? Which combinations must be rejected? How often do rules change? Who maintains them? How are failures located? The syntax should serve these needs.
Design Semantics First: Using Price Rules as an Example
Consider the rule order.total >= 100 USD. Before choosing between braces or indentation, you need to clarify whether total refers to the tax-inclusive or tax-exclusive amount, whether a missing value is an error or zero, whether different currencies can be compared, and whether values use integer cents or decimal fixed-point numbers.
If these are not defined, the system cannot provide stable answers even if the parser correctly accepts the input. The "domain-specific" nature of a DSL is first reflected in its types, constraints, and execution rules.
Implementing this Rule as a Minimal Executable Language
This example only supports "field >= non-negative amount currency". The domain model specifies that order.total is the determined order total in cents; only USD is accepted, no currency conversion is performed, and a missing total reports an error. The amount allows up to 9 integer digits and 2 decimal places, avoiding leaving arbitrary-length numbers and ambiguous rounding rules to the executor.
fromdataclassesimportdataclassimportre@dataclass(frozen=True)classRule:field:strcents:intcurrency:strspan:tuple[int, int]classRuleError(ValueError):passPATTERN=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})")defcompile_rule(source):iflen(source)>256:raiseRuleError("rule too long")match=PATTERN.fullmatch(source)ifmatchisNone:raiseRuleError("expected FIELD >= AMOUNT CURRENCY")field=match["field"]iffield!="order.total":raiseRuleError(f"unknown field at {match.span('field')}: {field}")ifmatch["currency"]!="USD":raiseRuleError(f"unsupported currency at {match.span('currency')}")fraction=(match["fraction"]or"").ljust(2,"0")cents=int(match["whole"])*100+int(fraction)returnRule(field,cents,"USD",match.span())defevaluate(rule,order):if"total_cents"notinorderororder["total_cents"]isNone:raiseRuleError("missing order.total")amount=order["total_cents"]iftype(amount)isnotintoramount<0:raiseRuleError("total must be nonnegative integer cents")iforder.get("currency")!=rule.currency:raiseRuleError("currency mismatch")returnamount>=rule.centsrule=compile_rule("order.total >= 100.00 USD")assertrule.cents==10000assertnotevaluate(rule,{"total_cents":9999,"currency":"USD"})assertevaluate(rule,{"total_cents":10000,"currency":"USD"})assertcompile_rule("order.total >= 0.1 USD").cents==10forsourcein["order.totla >= 100 USD","order.total >= 1 EUR","order.total >= 0.001 USD","order.total >= 1 USD trailing"]:try:compile_rule(source)exceptRuleError:passelse:raiseAssertionError("invalid rule accepted")fororderin[{},{"total_cents":None},{"total_cents":True,"currency":"USD"},{"total_cents":10000,"currency":"EUR"}]:try:evaluate(rule,order)exceptRuleError:passelse:raiseAssertionError("invalid input accepted")
This restricted language has no nested expressions, so a full-match regex is sufficient to express the current syntax; adding parentheses, and/or, and precedence requires designing explicit syntax and a parser, rather than continuously concatenating regexes to mask structure. fullmatch also ensures no trailing text is ignored after the rule.
Amount parsing directly converts decimal characters to integer cents; 0.1 is interpreted as 10 cents, and 0.001 is rejected rather than silently rounded. Execution uses type(amount) is int because Python's bool is a subclass of int, and this domain does not accept true as one cent. The rule semantics here are explicitly defined by examples, without borrowing the host language's implicit type conversion.
Diagnostic Precision Expands with Language Capability
Current field or currency errors preserve the match span, but syntax errors only provide a global hint; this is a minimal executor, not a full editor diagnostic. For interactive editing, you can preserve spans for both tokens and nodes, distinguishing between "missing amount after >=", "unsupported currency", and "rule incomplete", then map diagnostics back to the source code.
Input
Layer to Handle
Why Not All Return False
Total 99.99 USD
Execution returns false
Valid data, rule does not match
order.totla
Name check rejects
A typo makes the rule never take effect
0.001 USD
Current amount syntax rejects
Precision policy does not allow this input yet
Total missing
Execution input error
Missing data differs from being below the threshold
Total currency EUR
Domain compatibility error
No implicit currency conversion rule exists
If the business wants missing values to become unknown and use three-valued logic for and/or, this should be treated as another explicit language semantic. You cannot simply catch all exceptions in a backend and return false; otherwise, two backends will produce different answers for the same rule.
Parsing Succeeds, Why Can't All Failures Be Treated as False?
An editable rule is a better test of language boundaries than four "success/failure" buttons. The following inputs will actually go through full matching, field checking, currency checking, and execution input checking, before generating a restricted CompareAmount representation. Modifying the spelling, writing three decimal places, or leaving the total missing should fall into different error categories; only a valid amount below the threshold returns false.
Preparing the visual
Parsing Succeeds, Why Can't All Failures Be Treated as False?
Syntax, fields, units, and runtime data are different contracts. 99.99 failing the threshold is false; unknown fields or missing data are errors; amounts convert directly to integer cents, bypassing floating-point rounding.
{"id":"dsl-domain-contract","title":"Parsing Succeeds, Why Can't All Failures Be Treated as False?","summary":"Syntax, fields, units, and runtime data are different contracts. 99.99 failing the threshold is false; unknown fields or missing data are errors; amounts convert directly to integer cents, bypassing floating-point rounding.","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":"Order Total / cent","currency":"Order Currency","data":"Execution Input","field":"Field","heading":"Parsing Succeeds, Why Can't All Failures Be Treated as False?","intro":"Edit the amount rule, then change the order total, currency, or missing status. Each layer provides specific results; only valid data enters the comparison.","missing":"Order Total Missing","reset":"Reset","result":"Result or Error Category","rule":"Rule Text","scope":"Only accepts field >= non-negative amount currency, with up to 9 integer digits and 2 decimal places; field only order.total, currency only USD, amounts stored as integer cents. No nesting, exchange rates, or external execution.","scopeLabel":"Model Scope and Conventions","syntax":"Syntax","unit":"Currency"}}
Internal DSL: Borrowing Host Syntax and Tools
Internal or embedded DSLs leverage host mechanisms such as functions, operators, types, and closures to represent domain operations. For example, a query builder can combine field comparisons into an expression tree, which is eventually generated as parameterized SQL by the backend.
The host language's type checking is a foundational capability, but domain checking is not free. Only by encoding field types, operation constraints, and state transitions into the API can the compiler help reject corresponding misuse; directly accepting any string via where(string) does not automatically gain field name checking or SQL safety.
Builders can either execute immediately or save an intermediate representation for interpretation or compilation. Method chaining is just surface syntax; you cannot infer that it builds an AST from the chained style alone.
Expression Templates: Delayed Evaluation Also Requires a Correct Cost Model
Matrix libraries can use expression templates to preserve the structure of B + C * D, choosing loop fusion or optimized evaluation order. However, matrix multiplication, aliasing, and reuse may make temporary results more correct or efficient; you cannot claim that all expressions avoid allocating temporary matrices or automatically yield optimal code. Eigen Lazy Evaluation and Aliasing
Macros can also embed token syntax in the host language that requires separate parsing. Therefore, "internal DSLs necessarily have no parser" is not a general rule. Whether semantic checking occurs at the macro stage or in subsequent type checking depends on the interface design.
External DSL: Having Its Own Input Language
Standalone syntax is suitable for scenarios requiring cross-host language usage, targeting domain users, or strictly limiting expressive power. It needs to handle lexing, syntax, locations, diagnostics, and versioning itself, but does not need to generate machine code.
Common backends include directly interpreting the AST, lowering to a rule IR, generating SQL or host source code, generating bytecode, or compiling to machine code. The choice depends on execution frequency, deployment, debugging, portability, and restriction capabilities, not simply "compile if you want speed, transpile if you want debugging."
A configuration snippet can be checked at load time and repeatedly interpreted, which may already meet performance requirements; a compute-intensive DSL might be worth investing in IR optimization and specialized backends. Both are complete and reasonable designs.
Layering Frontend Checks
Layer
Example Issue
Appropriate Diagnostic
Lexical
Illegal characters in amount literal
Point out the specific character and allowed format
Syntax
Comparison operator missing right-hand value
Point out the missing item and current location
Name Resolution
order.totla does not exist
Point out the unknown field, optionally suggest candidate spellings
Type and Unit
Comparing amount with string
Explain types on both sides and allowed operations
Domain Constraints
Current rule does not allow accessing sensitive personal fields
Explain the restriction and rule context
Execution
Data missing or budget exhausted
Return a controlled error that can be linked to source code
Nodes should preserve source locations, and necessary mappings should be retained when lowering to IR. This allows generated code or runtime errors to point back to the rule written by the user, rather than just displaying internal temporary variables.
Types, Effects, and Resource Boundaries
Limiting expressive power makes verification easier. For example, prohibit arbitrary network and file calls, providing only explicitly defined pure functions; limit loops or use bounded set operations; set step, time, and memory budgets for execution.
But "being a DSL" does not mean it is already safely isolated. When embedding host execution, macro expansion, or calling external functions, you must still explicitly define their permissions and side effects. If rule sources are untrusted, simply concatenating strings and handing them to the host eval is not a sandbox.
For a DSL that generates SQL, values should be passed via bound parameters; identifiers should be handled according to syntax, quoting, and allowed sets. Parameterized values cannot directly replace arbitrary table names or column names.
Intermediate Representation Allows Multiple Entrances to Share Semantics
You can have a visual rule editor, a host API, and an external text parser all generate the same domain IR. Type checking, optimization, and execution share this representation, avoiding different semantics from three different entrances.
For example, the IR node CompareAmount(field, amount, currency) is easier to check, format, and serialize than an unparsed string fragment. When exporting the IR, you should also define a version; do not treat internal memory structures as a long-term compatible protocol.
Version Evolution: Rules Are Also Long-Term Data
After the language goes live, field deletions, default value changes, rounding rule adjustments, or keyword additions may change the meaning of old rules. You can record the language version for rules, provide parsing and migration strategies, and retain normalized representations and reproducible test cases.
Backward compatibility cannot just check "old text still parses." If an old rule parses to a different result, it is also a semantic break. Regression test cases should cover boundary values, missing values, and error inputs, and compare results between the interpreter and compilation backend.
Compare Results and Error Categories Before Migration
Assume the old version defaults missing amounts to zero, while the new version reports an error. Old text can still parse, but behavior has already changed. Migration test cases should save input data, rule version, expected results, or error categories, not just compare whether ASTs look the same.
The same set of cases can be handed to the interpreter, the SQL-generating backend, and other compilation backends. Normal results must be consistent; handling of missing data, currency mismatches, and precision boundaries must also be consistent. Normalized formats can change spacing or amount display, but cannot silently change the precision policy for 0.1 or the boundary for >=.
Quick Selection Guide
Requirement
Suggested Starting Point
Developers use it within the same language, standard APIs are easy to express
Functions, type-safe builders, or operator interfaces
Repeatedly generating host code, needing to read syntax structure
Macros or code generators
Cross-language distribution, domain user editing, independent version management
External syntax and stable semantic model
Multiple entrances sharing rule capabilities
Unified domain IR, connected to different frontends
Internal DSLs do not necessarily evolve into external languages, and external languages are not the "higher-level" endpoint. Start with minimal expressive power to cover real tasks, then expand based on explicit needs.