Skip to content

VOL 09 / CH 06 / LESSON 03

6.3 DSL Design: Syntax, Semantics, Tooling, and Security Boundaries ​

Once promotion configuration needs nested conditions, references, priorities, and expressions, it is doing the work of a small language. Keeping the JSON wrapper does not make its grammar and semantics maintain themselves.

A domain-specific language, or DSL, lets users express intent within a limited domain. An attractive success example is a start; useful diagnostics and compatibility with old rules usually demand more design work.

First, Determine Whether a Language Is Actually Needed ​

The following requirements may not necessitate a DSL:

  • There are only a small number of fixed options: configuration objects or forms will suffice;
  • The primary users are developers: general-purpose libraries or typed APIs are often better;
  • Changes involve only parameters, not control structures: data configuration is sufficient;
  • Rules must be reviewed and controlled through code reviews and deployments: using the host language directly may offer greater transparency.

Combining conditions, expressing calculations, or reusing rules can justify considering a DSL. Independent rule releases may strengthen the case, but they are not a requirement: an internal DSL can compile and ship with its application.

Internal DSLs and External DSLs ​

Internal DSLs ​

Internal DSLs use the host language's syntax and types. This is a proposed API usage sketch: Policy and its constructors still need implementations, including an explicit currency or a documented default:

java
Policy policy = Policy.when(
        all(country("CN"), orderAmountAtLeast("200.00")))
    .then(applyDiscount("20.00"))
    .otherwise(noDiscount());

The advantage is full reusability of the host language's compiler, IDE, debugger, and module system. The downside is that the expression is constrained by the host language's grammar, and non-developers still face the underlying code environment. Additionally, callers may bypass the intended composition constraints.

External DSLs ​

text
rule summer-sale priority 20 {
  when country == "CN" and order.amount >= 200.00 CNY
  then discount 20.00 CNY
}

External DSLs can closely align with domain-specific language constructs and are independently versioned. However, you must provide a complete toolchain: parser, type checking, error reporting, formatting, editor support, debugging, migration, and security boundaries.

From Text to Execution Requires Clear Stages ​

text
Source text
  ↓ Lexer / Parser
Syntax tree AST
  ↓ Name resolution and type checking
Validated model
  ↓ Lowering / planning
Intermediate representation IR
  ↓ Interpret, compile, or translate
Execution result

Breaking these stages apart prevents the parser from directly executing business logic. Parsing only answers the question "Is the structure valid?"; semantic analysis then answers "Do variables exist? Are types compatible? Can units be added together?"; and the executor accepts a statically checked model. Missing runtime input, external-service failures, and resource exhaustion still need explicit handling.

text
200.00 CNY + 10 %

This may be syntactically valid, but semantically it cannot be directly added. The DSL should report this type error when rules are deployed, before transaction execution. That does not eliminate other runtime failures.

Syntax Should Serve Error Information ​

When designing syntax, don't just show successful examples, also list the most common mistakes:

  • Missing parentheses;
  • Unknown fields;
  • Comparing strings with numbers;
  • Duplicate rule names;
  • Unreachable branches;
  • Recursive references or circular dependencies.

Error messages should include the location, root cause, and actionable fix suggestions:

text
promotion.rules:8:21
The type of order.amount is Money<CNY>, so it cannot be compared with the integer 200.
Fix: change to order.amount >= 200.00 CNY

"parse error near token" is useful for language authors but offers little value to domain users.

Put Semantics into Domain Models ​

Represent operations with explicit node types. These Java 17 model declarations can go inside one class, with imports for java.math.BigDecimal and java.util.*. They are not yet a parser or complete interpreter:

java
sealed interface Condition permits All, Any, CountryIs, AmountAtLeast {}

record All(List<Condition> children) implements Condition {
    All { children = List.copyOf(children); }
}
record Any(List<Condition> children) implements Condition {
    Any { children = List.copyOf(children); }
}
record CountryIs(String countryCode) implements Condition {
    CountryIs {
        if (countryCode == null || !countryCode.matches("[A-Z]{2}")) {
            throw new IllegalArgumentException("two uppercase country letters required");
        }
    }
}
record Money(BigDecimal amount, Currency currency) {
    Money {
        Objects.requireNonNull(amount, "amount");
        Objects.requireNonNull(currency, "currency");
    }
}
record AmountAtLeast(Money minimum) implements Condition {
    AmountAtLeast {
        Objects.requireNonNull(minimum, "minimum");
        if (minimum.amount().signum() < 0) {
            throw new IllegalArgumentException("negative minimum");
        }
    }
}

These constructors check local constraints and copy child lists so callers cannot mutate them after parsing. The country check accepts any two uppercase letters; business allowlists, currency compatibility, and whole-tree limits still need validation.

The parser first builds a structured AST. Name resolution and type checking can then produce a typed model, followed by lowering to a simpler IR. This approach enables optimization of the executor without altering the user's grammar and allows multiple frontend syntaxes to share the same semantic core.

For this promotion model, one possible contract is: compare money only in the same currency using BigDecimal.compareTo; evaluate All and Any left to right with short-circuiting; let empty All be true and empty Any be false. Under these rules, 200.0 CNY meets a 200.00 CNY threshold, while 200.00 USD cannot silently convert to CNY for the comparison.

The language should also explicitly define:

  • Numeric precision, rounding behavior, and currency units;
  • Time intervals and time zones;
  • How null or missing fields propagate;
  • Rule conflicts and their resolution priorities;
  • Evaluation order and short-circuiting behavior;
  • Whether external data access is permitted and how failures should be represented.

Executing Untrusted Rules Requires Budgeting ​

Never pass user-provided expressions directly to the host language's eval. Blocking a few function names does not address reflection, object traversal, or resource consumption. Python's ast.literal_eval does not execute arbitrary expressions, but hostile input can still exhaust memory or stack space; it is not a resource sandbox for untrusted input.

A controlled interpreter must at least provide:

  • A whitelist of allowed syntax and built-in functions;
  • Maximum AST depth and source file size;
  • Execution budgets for instruction count, recursion depth, or runtime;
  • Limits on memory usage and output size;
  • A default environment with no network, file, or process permissions;
  • Cancelable execution;
  • Audit logging and rule versioning.

Budgets must cover parsing, semantic checks, and execution. Checking AST depth after a deeply nested input has already exhausted the parser is too late. An evaluator can charge a step before visiting each node, but built-in operations must also be bounded or cancelable; one node containing unlimited work defeats the step budget.

When rules originate from untrusted tenants, in-process timeouts are often insufficient for strong isolation. Consider running rules in isolated processes, containers, or dedicated sandboxes, and define narrow, well-scoped protocols between the host and the executor.

Version Evolution Is the Primary Cost of DSLs ​

Once rules are saved, the old syntax becomes a data format. When releasing a new version, the following questions must be answered:

  • Do existing rules continue to execute?
  • How are deprecated syntaxes warned about and automatically migrated?
  • Can execution results be replayed?
  • Do the same rules produce identical outcomes across different interpreter versions?
  • What is stored in production logs: the original text, the AST, the IR, or all three plus version metadata?
  • Can a rollback operation read and apply the new rules?

Store the language version and content hash with each rule. Treating compiled output as a reproducible cache also requires access to the matching compiler, options, and dependencies. Replaying results requires fixed inputs, external-data versions, and clock values where relevant; rule text alone is insufficient.

Test a Small Language ​

Cover at least four layers:

  1. Parsing Tests: Valid and invalid syntax, error positioning;
  2. Semantic Tests: Types, names, units, conflicts;
  3. Execution Tests: Boundary values, short-circuiting, determinism;
  4. Compatibility Tests: Behavior of historical rules and corpora under new versions.

Property-based tests can also search for counterexamples:

  • Semantics remain unchanged after formatting and re-parsing;
  • Intermediate representation (IR) outputs are consistent before and after optimization;
  • Expressions of varied shapes and depths finish within budget or are explicitly rejected.

Finite tests cannot prove termination for every input. Check that every recursive or looping path consumes budget and that each primitive is bounded, then verify that failure leaves no partially applied business effects.

Completion Checklist ​

Design a minimal DSL for promotional rules that supports only country, order amount, and discount:

  1. Provide three valid rules and five invalid ones;
  2. Define type rules for Money and percentage;
  3. Draw the transformation flow from source code to AST, IR, and final execution result;
  4. Specify a deterministic method for resolving conflicting rules;
  5. Establish execution budget, versioning fields, and migration strategies.

If the only successful example is a single paragraph that "looks like natural language," the DSL design is still incomplete.

References ​

Built with VitePress | Software Systems Atlas