Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More docs and add Charms.Intrinsic #36

Merged
merged 16 commits into from
Oct 7, 2024
6 changes: 6 additions & 0 deletions .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@ jobs:
- name: Benchmark sort
run: |
mix run bench/sort_benchmark.exs
- name: Document
run: |
mix docs
- name: Package
run: |
mix archive.build
1 change: 1 addition & 0 deletions bench/vec_add_int_list.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule AddTwoIntVec do
@moduledoc false
use Charms
alias Charms.{SIMD, Term, Pointer}

Expand Down
62 changes: 55 additions & 7 deletions lib/charms.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
defmodule Charms do
@moduledoc """
Documentation for `Charms`.

## `defm` and intrinsic
There are two ways to define a function with `defm/2` or implement callbacks of `Charms.Intrinsic` behavior. The `defm/2` is a macro that generates a function definition in Charm. The intrinsic is a behavior that generates a function definition in MLIR.

The intrinsic is more flexible than `defm` because:
- Intrinsic can be variadic and its argument can be anything
- Intrinsic is suitable for the cases where directly writing or generating MLIR is more ideal
- An intrinsic should be responsible for its type check while the Charm’s type system is responsible for function call’s type check

The `defm` is more suitable for simple functions because it is designed to be as close to vanilla Elixir as possible. As a rule of thumb, use `defm` for simple functions and intrinsic for complex functions or higher-order(generic) function with type as argument.

## `defm`'s differences from `Beaver.>>>/2` op expressions
- In `Beaver.>>>/2`, MLIR code are expected to mixed with regular Elixir code. While in `defm/2`, there is only Elixir code (a subset of Elixir, to be more precise).
- In `defm/2`, the extension of the compiler happens at the function level (define your intrinsics or `defm/2`s), while in `Beaver.>>>/2`, the extension happens at the op level (define your op expression).
- In `Beaver.>>>/2` the management of MLIR context and other resources are done by the user, while in `defm/2`, the management of resources are done by the `Charms` compiler.
- In `defm/2`, there is expected to be extra verifications built-in to the `Charms` compiler (both syntax and types), while in `Beaver.>>>/2`, there is none.

## Caveats and limitations

We need a explicit `call` in function call because the `::` special form has a parser priority that is too low so a `call` macro is introduced to ensure proper scope.
"""

defmacro __using__(opts) do
quote do
import Charms.Defm
import Charms
use Beaver
require Beaver.MLIR.Dialect.Func
alias Beaver.MLIR.Dialect.{Func, Arith, LLVM, CF}
Expand Down Expand Up @@ -39,13 +59,41 @@ defmodule Charms do
end
end

def child_spec(mod, opts \\ [])
@doc """
define a function that can be JIT compiled
"""
defmacro defm(call, body \\ []) do
{call, ret_types} = Charms.Defm.decompose_call_and_returns(call)

call = Charms.Defm.normalize_call(call)
{name, args} = Macro.decompose_call(call)
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved

def child_spec(mods, opts) when is_list(mods) do
%{id: Module.concat(mods), start: {Charms.JIT, :init, [mods, opts]}}
end
{:ok, env} =
__CALLER__ |> Macro.Env.define_import([], Charms.Defm, warn: false, only: :macros)
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved

def child_spec(mod, opts) do
%{id: mod, start: {Charms.JIT, :init, [mod, opts]}}
[_enif_env | invoke_args] = args

invoke_args =
for {:"::", _, [a, _t]} <- invoke_args do
a
end

quote do
@defm unquote(Macro.escape({env, {call, ret_types, body}}))
def unquote(name)(unquote_splicing(invoke_args)) do
if @init_at_fun_call do
{_, %Charms.JIT{}} = Charms.JIT.init(__MODULE__)
end

f =
&Charms.JIT.invoke(&1, {unquote(env.module), unquote(name), unquote(invoke_args)})

if engine = Charms.JIT.engine(__MODULE__) do
f.(engine)
else
f
end
end
end
end
end
43 changes: 1 addition & 42 deletions lib/charms/defm.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,47 +51,6 @@ defmodule Charms.Defm do
"""
defmacro cond_br(_condition, _clauses), do: :implemented_in_expander

@doc """
define a function that can be JIT compiled

## Differences from `Beaver.>>>/2` op expressions
- In `Beaver.>>>/2`, MLIR code are expected to mixed with regular Elixir code. While in `defm/2`, there is only Elixir code (a subset of Elixir, to be more precise).
- In `defm/2`, the extension of the compiler happens at the function level (define your intrinsics or `defm/2`s), while in `Beaver.>>>/2`, the extension happens at the op level (define your op expression).
- In `Beaver.>>>/2` the management of MLIR context and other resources are done by the user, while in `defm/2`, the management of resources are done by the compiler.
- In `defm/2`, there is expected to be extra verifications built-in to the compiler (both syntax and types), while in `Beaver.>>>/2`, there is none.
"""
defmacro defm(call, body \\ []) do
{call, ret_types} = decompose_call_and_returns(call)

call = normalize_call(call)
{name, args} = Macro.decompose_call(call)
env = __CALLER__
[_enif_env | invoke_args] = args

invoke_args =
for {:"::", _, [a, _t]} <- invoke_args do
a
end

quote do
@defm unquote(Macro.escape({env, {call, ret_types, body}}))
def unquote(name)(unquote_splicing(invoke_args)) do
if @init_at_fun_call do
{_, %Charms.JIT{}} = Charms.JIT.init(__MODULE__)
end

f =
&Charms.JIT.invoke(&1, {unquote(env.module), unquote(name), unquote(invoke_args)})

if engine = Charms.JIT.engine(__MODULE__) do
f.(engine)
else
f
end
end
end
end

@doc false
def decompose_call_and_returns(call) do
case call do
Expand All @@ -101,7 +60,7 @@ defmodule Charms.Defm do
end

@doc false
defp normalize_call(call) do
def normalize_call(call) do
{name, args} = Macro.decompose_call(call)

args =
Expand Down
3 changes: 2 additions & 1 deletion lib/charms/env.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Charms.Env do
use Beaver
use Charms.Intrinsic
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved

@impl true
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved
def handle_intrinsic(:t, [], opts) do
Beaver.ENIF.Type.env(opts)
end
Expand Down
16 changes: 16 additions & 0 deletions lib/charms/intrinsic.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Charms.Intrinsic do
@moduledoc """
Behaviour to define intrinsic functions.
"""
alias Beaver
@type opt :: {:ctx, MLIR.Context.t()} | {:block, MLIR.Block.t()}
@type opts :: [opt | {atom(), term()}]
@callback handle_intrinsic(atom(), [term()], opts()) :: term()

defmacro __using__(_) do
quote do
@behaviour Charms.Intrinsic
use Beaver
end
end
end
3 changes: 2 additions & 1 deletion lib/charms/pointer.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
defmodule Charms.Pointer do
use Beaver
use Charms.Intrinsic
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved
alias Beaver.MLIR.{Type, Attribute}
alias Beaver.MLIR.Dialect.{Arith, LLVM, Index}
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved

@impl true
def handle_intrinsic(:allocate, [elem_type], opts) do
handle_intrinsic(:allocate, [elem_type, 1], opts)
end
Expand Down
3 changes: 2 additions & 1 deletion lib/charms/prelude.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule Charms.Prelude do
use Beaver
use Charms.Intrinsic
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved
alias Beaver.MLIR.Dialect.{Arith, Func}
@enif_functions Beaver.ENIF.functions()
@binary_ops [:!=, :-, :+, :<, :>, :<=, :>=, :==, :&&, :*]
Expand Down Expand Up @@ -31,6 +31,7 @@ defmodule Charms.Prelude do
v
end

@impl true
def handle_intrinsic(:result_at, [%MLIR.Value{} = v, i], _opts) when is_integer(i) do
v
end
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
3 changes: 2 additions & 1 deletion lib/charms/simd.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
defmodule Charms.SIMD do
use Beaver
use Charms.Intrinsic
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved
alias MLIR.Dialect.Arith
alias MLIR.Type

@impl true
def handle_intrinsic(:new, [type, width], opts) do
fn literal_values ->
mlir ctx: opts[:ctx], block: opts[:block] do
Expand Down
3 changes: 2 additions & 1 deletion lib/charms/term.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Charms.Term do
use Beaver
use Charms.Intrinsic
jackalcooper marked this conversation as resolved.
Show resolved Hide resolved

@impl true
def handle_intrinsic(:t, [], opts) do
Beaver.ENIF.Type.term(opts)
end
Expand Down
Loading