The Design of Intrinsic #54
Replies: 0 comments 9 replies
-
I think that the first option is a bit too implicit. What I think would be nice is for a module that defines an intrinsic to have to declare it explicitly. The I assume the Beaver/Charms compiler is able to try and call an undefined function as an intrinsic, but ideally, you would at least have a "private name" like we have for defn: https://github.com/elixir-nx/nx/blob/9e2cd048de610151b85a27a183035bc0873fa77f/nx/lib/nx/defn/compiler.ex#L797 This way, intrinsics of the form |
Beta Was this translation helpful? Give feedback.
-
@polvalente I wanted to have this kind of function definition convention. But the problem in this case is that, there are extra arguments, like |
Beta Was this translation helpful? Give feedback.
-
POC of new implementationdefmodule M do
defmacro __using__(_) do
quote do
import M
@before_compile M
Module.register_attribute(__MODULE__, :intrinsic, accumulate: true)
end
end
defmacro __before_compile__(_env) do
quote bind_quoted: [] do
@all_intrinsics @intrinsic |> Enum.reverse() |> List.flatten() |> Enum.uniq()
for {name, _arity, intrinsic_name} <- @all_intrinsics do
def __intrinsics__(unquote(name)) do
unquote(intrinsic_name)
end
end
def __intrinsics__() do
@all_intrinsics
end
end
end
defmacro defintrinsic(call, opts \\ [], do: body) do
{name, _meta, args} = call
arity = length(args)
# {opts, args} = List.pop_at(args, arity - 1)
# arity = arity - 1
intrinsic_name = :"__defintrinsic_#{name}__"
opts_ast =
for {k, v} <- opts do
quote do
unquote(v) = opts[unquote(k)]
end
end
quote do
@intrinsic {unquote(name), unquote(arity), unquote(intrinsic_name)}
def unquote(intrinsic_name)(unquote(args), opts) do
unquote(opts_ast)
unquote(body)
end
def unquote(name)(unquote_splicing(args)) do
raise "Intrinsic #{unquote(name)}/#{unquote(arity)} cannot be called outside of a defm context"
end
end
|> dbg()
end
end
defmodule Pointer do
use M
defintrinsic allocate(elem_type), [a: a, b: b, nested: %{c: c}] do
{a, b, c, elem_type, 123}
end
defintrinsic allocate(elem_type, length), [c: c] do
{123, c}
end
defintrinsic load(out, elem_type) do
123
end
end
Pointer.__defintrinsic_allocate__([:type], [a: 1, b: 2, nested: %{c: 3}])
Pointer.__defintrinsic_allocate__([:type, 1], [c: 2])
Pointer.__defintrinsic_load__([:out, :type], []) |
Beta Was this translation helpful? Give feedback.
-
@doc """
Allocates a single element of the given `elem_type`, returning a pointer to it.
"""
defintrinsic allocate(elem_type) do
quote do
Charms.Pointer.allocate(unquote(elem_type), 1)
end
end
@doc """
Allocates an array of `size` elements of the given `elem_type`, returning a pointer to it.
"""
defintrinsic allocate(elem_type, size), %Opts{ctx: ctx, args: [_elem_type, size_v]} do
cast =
case size_v do
i when is_integer(i) ->
quote do
const unquote(size_v) :: i64()
end
%MLIR.Value{} ->
if MLIR.equal?(MLIR.Value.type(size_v), Type.i64(ctx: ctx)) do
size
else
quote do
value arith.extsi(unquote(size)) :: i64()
end
end
end
quote do
size = unquote(cast)
value llvm.alloca(size, elem_type: unquote(elem_type)) :: Pointer.t()
end
end |
Beta Was this translation helpful? Give feedback.
-
New version
@doc """
Allocates a single element of the given `elem_type`, returning a pointer to it.
"""
defintrinsic allocate(elem_type) do
quote bind_quoted: [elem_type: elem_type] do
Charms.Pointer.allocate(elem_type, 1)
end
end
@doc """
Allocates an array of `size` elements of the given `elem_type`, returning a pointer to it.
"""
defintrinsic allocate(elem_type, size) do
%Opts{ctx: ctx} = __IR__
cast =
case size do
i when is_integer(i) ->
quote bind_quoted: [size: i] do
const size :: i64()
end
%MLIR.Value{} ->
if MLIR.equal?(MLIR.Value.type(size), Type.i64(ctx: ctx)) do
size
else
quote bind_quoted: [size: size] do
value arith.extsi(size) :: i64()
end
end
end
quote bind_quoted: [elem_type: elem_type, size: cast] do
value llvm.alloca(size, elem_type: elem_type) :: Pointer.t()
end
end |
Beta Was this translation helpful? Give feedback.
-
RFC, the design of intrinsic
What is it?
Intrinsic is the feature in Charms that allows user to define a special kind of macro to generate
defm
AST but with access to MLIR CAPIs (provided by Beaver or other MLIR libraries in Elixir).With that, Charms becomes more powerful and flexible to generate MLIR, on par with Mojo's meta-programming capability.
Use intrinsic to formalize Charms'
defm
For a DSL, it is essential to define its syntax and semantics in a simple and comprehensive way.
Here is the rule of thumb of the atomic elements
defm
in Charms, or we can call it the leaf node of AST, basic node that no longer gets expanded:defm
:f32()
,i32()
, etc.Here is the problem, with this design we manage to to keep the end-user experience consistent and simple, there is a gap between what
defm
itself and MLIR's basic entities. Besides type and value, MLIR has region, block, operation and attribute, etc. We need a way to bridge this gap, and intrinsic is the solution that emerged during the development of Charms.Intrinsic is supposedly to be used by library authors or advanced users, who want to define a new kind of macro that can generate AST with access to MLIR CAPIs. It is a way to extend Charms'
defm
to cover more MLIR entities beyond its syntax.Case study:
Pointer.allocate/2
we want to define a new intrinsic
Pointer.allocate/2
to allocate memory on stack, with the following signature:if size is 1, omit it
Pointer.allocate/2
should be able to convert the size to i64 if it is not, and generate the AST for alloca operation.And here are the two options to implement it:
Option 1, callback style
There should generate AST like this:
Option 2, def style
with this design, we have a second call in the definition, whose name must be
params
Beta Was this translation helpful? Give feedback.
All reactions