Defining dynamic dialects with Slang
Copy MarkdownBeaver.Slang defines MLIR types, attributes, and operations in Elixir, then
builds an IRDL schema that MLIR can verify and load at runtime. The declaration
names are preserved in the schema, so diagnostics and generated IR remain
readable.
A complete dialect
defmodule Geometry do
use Beaver.Slang, name: "geometry"
defconstraint integer_like do
all_of([base("!builtin.integer"), any()])
end
defconstraint direction_value do
any_of([
Beaver.MLIR.Attribute.string("left"),
Beaver.MLIR.Attribute.string("right")
])
end
deftype index(element = ^integer_like)
deftype token()
defattr direction(value = ^direction_value)
defop consume_token(value = base(token()))
defop sequence(head = any(), tail = variadic(any()), fallback = optional(any())),
results: [value: optional(any())]
defop scope(),
attributes: [label: base("#builtin.string")],
regions: [body: {:region, args: [], size: 1}],
traits: [:isolated_from_above, :no_terminator]
defop yield(), traits: [:terminator]
endBuild and inspect the schema without changing the context's registered dynamic dialects:
schema = Geometry.__slang_dialect__(ctx)
Beaver.MLIR.verify!(schema)
IO.puts(Beaver.MLIR.to_string(schema, generic: true))Load it when operations, types, or attributes from the dialect are needed:
result = Beaver.Slang.load(ctx, Geometry)
true = Beaver.MLIR.LogicalResult.success?(result)Constraints
any() accepts any type or attribute. is(value) requires one exact MLIR type
or attribute. any_of(list) and all_of(list) compose constraints, including
constraints already produced by another combinator.
base("!dialect.type") and base("#dialect.attribute") constrain the base type
or attribute without constraining its parameters. A Slang type or attribute
constructor can also supply the base reference, as in base(token()) above.
Use defconstraint for a reusable constraint and reference it with ^name.
defalias remains available as an equivalent concise spelling.
Names and cardinality
Names are inferred from assignments and variables:
deftype pair(left = any(), right = any())
defop add(lhs = any(), rhs = any()), results: [sum: any()]For expressions that do not bind a variable, pass parameter_names,
operand_names, or result_names explicitly:
deftype pair(any(), any()), parameter_names: [:left, :right]
defop add(any(), any()),
operand_names: [:lhs, :rhs],
results: [any()],
result_names: [:sum]Operation attributes and regions are keyword lists, so their keys are their
schema names. optional and variadic apply to operation operands and results,
which are the entities for which upstream IRDL exposes variadicity. Type and
attribute parameters, operation attributes, and regions are single entries.
Regions accept these descriptors:
:anyallows any region shape.{:sized, positive_integer}constrains the block count.{:region, args: constraints, size: positive_integer}constrains entry-block arguments, block count, or both. Omit either option to leave it unconstrained.
Schema and runtime interfaces
The IRDL module and external operation interfaces have different lifetimes.
__slang_dialect__/1 only constructs the schema. Beaver.Slang.load/2
canonicalizes and verifies it, registers the dialect, and only then attaches
the requested built-in dynamic traits:
:terminator:isolated_from_above:no_terminator
Keeping attachment out of schema construction makes the IRDL module safe to inspect, serialize, and test without mutating dialect registration. It also makes failures attributable to either schema verification or interface attachment instead of interleaving the two phases.
External operation interfaces
The interfaces: option attaches MLIR fallback models implemented by Elixir
callbacks. This lets dynamic operations participate in analyses, transforms,
and rewrites without a C++ or TableGen definition:
defmodule Effects do
use Beaver.Slang, name: "effects"
def pure(_operation), do: :pure
def speculatable(_operation), do: :speculatable
def forward_apply(operation, _rewriter, _results, state) do
handle = Beaver.MLIR.CAPI.mlirOperationGetOperand(operation, 0)
payload = Beaver.MLIR.TransformOpInterface.payload_ops(state, handle)
{:ok, %{0 => {:ops, payload}}}
end
def forward_effects(operation, effects) do
operand = Beaver.MLIR.CAPI.mlirOperationGetOpOperand(operation, 0)
result = Beaver.MLIR.Operation.result(operation, 0)
Beaver.MLIR.MemoryEffects.only_reads_handle(effects, operand)
Beaver.MLIR.MemoryEffects.produces_handle(effects, result)
Beaver.MLIR.MemoryEffects.only_reads_payload(effects)
end
defop constant(),
interfaces: [
memory_effects: &__MODULE__.pure/1,
conditionally_speculatable: &__MODULE__.speculatable/1
]
defop forward(handle = base("!transform.any_op")),
do: [base("!transform.any_op")],
interfaces: [
memory_effects: &__MODULE__.forward_effects/2,
transform_op: [apply: &__MODULE__.forward_apply/4]
]
endThe supported keys are:
:memory_effectsaccepts a callback of arity one that returns:pureor a list ofBeaver.MLIR.MemoryEffectsspecifications. An arity-two callback may instead add transform handle effects to its borrowed effects list.:conditionally_speculatablereturns:not_speculatable,:speculatable, or:recursively_speculatable.:transform_oprequires an:applycallback of arity four and accepts an optional:allows_repeated_handle_operandscallback. Apply callbacks map op results with{:ops, values},{:values, values}, or{:params, values}.:pattern_descriptorrequires a:populate_patternscallback of arity two and accepts a state-aware callback of arity three. Add patterns throughBeaver.MLIR.RewritePatternSet.
Each attachment has a dedicated BEAM callback process and belongs to one MLIR
context. Native callers wait outside normal BEAM schedulers, callback waits are
bounded to 30 seconds by default, and Beaver.MLIR.Context.destroy/1 releases
the model and its callback process. Direct attach functions accept a
:timeout option.
Operations, effect lists, transform rewriters, transform results and states, and rewrite-pattern sets delivered to callbacks are borrowed. They are valid only until that callback returns: do not send them to another process, store them, or use them later. A callback may use the ordinary Beaver APIs during that interval, but it must not synchronously re-enter the same interface attachment. Context multithreading must remain enabled for callback-backed interfaces.
Callback exceptions are emitted as MLIR diagnostics at the operation location and logged on the BEAM side. The native fallback is conservative: failed memory-effect callbacks add an unknown write, failed speculation callbacks are not speculatable, and failed transform callbacks return a definite failure.
Dynamic dialect and trait registration is local to an MLIR.Context; it is not
stored in IRDL or operation bytecode. Call Beaver.Slang.load/2 once for every
new context before parsing text or reading bytecode that uses the dialect.
Slang assigns every generated IRDL operation the source location of its
declaration. Invalid nested constraints therefore report the deftype,
defattr, or defop line rather than an implementation line inside Slang.