├── .credo.exs ├── .formatter.exs ├── .gitignore ├── .iex.exs ├── LICENSE ├── README.md ├── lib ├── kamex.ex └── kamex │ ├── exceptions.ex │ ├── interpreter.ex │ ├── interpreter │ ├── builtins.ex │ ├── builtins │ │ ├── fold.ex │ │ ├── lists.ex │ │ └── math.ex │ └── special_forms.ex │ ├── parser.ex │ ├── util.ex │ └── util │ ├── comb.ex │ └── math.ex ├── mix.exs ├── mix.lock ├── src └── lexer.xrl └── test ├── kamex.exs └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["lib/", "priv/", "test/"], 7 | excluded: [] 8 | }, 9 | color: true, 10 | checks: [ 11 | {Credo.Check.Refactor.Nesting, false}, 12 | {Credo.Check.Refactor.CyclomaticComplexity, false}, 13 | {Credo.Check.Readability.LargeNumbers, only_greater_than: 99_999} 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | import_deps: [:typed_struct], 4 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /cover/ 3 | /deps/ 4 | /doc/ 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | kamex-*.tar 9 | /tmp/ 10 | .elixir_ls 11 | src/*.erl -------------------------------------------------------------------------------- /.iex.exs: -------------------------------------------------------------------------------- 1 | Application.put_env(:elixir, :ansi_enabled, true) 2 | IEx.configure(inspect: [charlists: :as_lists]) 3 | import Kamex.Interpreter 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Ovyerus 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kamex 2 | 3 | > A basic Lisp interpreter implemented in Elixir. 4 | 5 | Currently implements a very simple Lisp with a tiny amount of builtin functions, 6 | but eventually plans to expand to be a Elixir implementation of the brilliant 7 | [KamilaLisp](https://github.com/kspalaiologos/kamilalisp) 8 | 9 | ## Known Issues 10 | 11 | - Probably a whole ton of stuff, it's very early days for this. Please open an 12 | issue if you notice weird behaviour. 13 | 14 | ## Builtins 15 | 16 | **Note:** this list is out of date. I am currently working on bringing Kamex up 17 | to feature-parity with KamilaLisp, so I will update this once I've caught up and 18 | can properly list stuff categorically. 19 | 20 | - +, -, \*, /, ++, --, ! 21 | - list, cons, append, head, tail 22 | - print, zerop 23 | - quote, lambda, def (global vars), defun (global func), let (locals in a 24 | block), if, or, and, not 25 | 26 | ## Examples 27 | 28 | ```elixir 29 | iex> run(~S[ 30 | ...> (defun add (x y) (+ x y)) 31 | ...> (add 6 9) 32 | ...> ]) 33 | {15, %{add: #Function<2.88664320/2 in Kamex.Interpreter.SpecialForms.lambda/3>}} 34 | ``` 35 | 36 | ```elixir 37 | iex> run(~S[ 38 | ...> (let ((x (+ 2 5)) (y (- 45 12))) (* x y)) 39 | ...> ]) 40 | {231, %{}} 41 | ``` 42 | 43 | ```elixir 44 | iex> run(~S[ (at $(-) $(= 0 (% _ 2)) (iota 100)) ]) 45 | {[0, 1, -2, 3, -4, 5, -6, 7, -8, 9, -10, 11, -12, 13, -14, 15, -16, 17, -18, 19, 46 | -20, 21, -22, 23, -24, 25, -26, 27, -28, 29, -30, 31, -32, 33, -34, 35, -36, 47 | 37, -38, 39, -40, 41, -42, 43, -44, 45, -46, 47, -48, ...], %{}} 48 | ``` 49 | 50 | ```elixir 51 | iex> run(~S[ 52 | ...> (defun factorial (n) 53 | ...> (if (= 0 n) 1 54 | ...> ($(* n)@factorial@$(- _ 1) n))) 55 | ...> 56 | ...> (factorial 10) 57 | ...> ]) 58 | {3628800, 59 | %{ 60 | factorial: #Function<2.104658454/2 in Kamex.Interpreter.SpecialForms.lambda/3> 61 | }} 62 | ``` 63 | 64 | ## Using 65 | 66 | - Install [Elixir](https://elixir-lang.org/) 67 | - `iex -S mix` to launch into the Elixir REPL (Native Kamex REPL soon™️) 68 | - `import Kamex.Interpreter` to import the interpreter function 69 | - `run(~S[(code here)])` for running code. 70 | 71 | ## License 72 | 73 | [MIT License](./LICENSE) 74 | -------------------------------------------------------------------------------- /lib/kamex.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex do 2 | @moduledoc """ 3 | Documentation for `Kamex`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> Kamex.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/kamex/exceptions.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Exceptions do 2 | @moduledoc false 3 | 4 | defmodule ArityError do 5 | defexception [:message] 6 | end 7 | 8 | defmodule UnknownFunctionError do 9 | defexception [:message] 10 | end 11 | 12 | defmodule UnknownLocalError do 13 | defexception [:message] 14 | end 15 | 16 | defmodule UnbalancedParensError do 17 | defexception [:message] 18 | end 19 | 20 | defmodule IllegalTypeError do 21 | defexception [:message] 22 | end 23 | 24 | defmodule MathError do 25 | defexception [:message] 26 | end 27 | 28 | defmodule ParserError do 29 | # TODO: enhance to smth link `UnexpectedTokenError` and add line number 30 | defexception [:message] 31 | end 32 | 33 | defmodule SyntaxError do 34 | defexception [:message] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/kamex/interpreter.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Interpreter do 2 | @moduledoc false 3 | alias Kamex.{Exceptions, Parser} 4 | alias Kamex.Interpreter.{Builtins, SpecialForms} 5 | 6 | # TODO: redo stuff to revolve around true/false instead of empty lists lol 7 | 8 | def to_ast(input) when is_binary(input) do 9 | with input <- String.to_charlist(input), 10 | {:ok, tokens, _} <- :lexer.string(input), 11 | :ok <- check_tokens(tokens) do 12 | Parser.parse(tokens) 13 | end 14 | end 15 | 16 | def run(input, locals \\ %{}) when is_binary(input) do 17 | ast = to_ast(input) 18 | 19 | # TODO: store env/locals in an Agent? 20 | Enum.reduce( 21 | ast, 22 | {nil, locals}, 23 | fn node, {_, locals} -> 24 | compute_expr(node, locals, true) 25 | end 26 | ) 27 | end 28 | 29 | def compute_expr(node, locals \\ %{}, ret_locals \\ false) 30 | 31 | def compute_expr([ident | args], locals, ret_locals) when is_atom(ident) do 32 | # TODO: completely redo locals returning, and add globals alongside I think 33 | 34 | {result, locals} = 35 | cond do 36 | SpecialForms.special_form?(ident) -> 37 | SpecialForms.run(ident, args, locals) 38 | 39 | Builtins.builtin?(ident) -> 40 | Builtins.run(ident, args, locals) 41 | 42 | is_function(locals[ident]) -> 43 | args = Enum.map(args, &compute_expr(&1, locals)) 44 | {locals[ident].(args, locals), locals} 45 | 46 | true -> 47 | raise Exceptions.UnknownFunctionError, 48 | message: "undefined function `#{ident}`" 49 | end 50 | 51 | if ret_locals, 52 | do: {result, locals}, 53 | else: result 54 | end 55 | 56 | def compute_expr([head | tail], locals, _ret_locals) do 57 | {head_result, locals} = compute_expr(head, locals, true) 58 | 59 | cond do 60 | is_function(head_result) -> head_result.(Enum.map(tail, &compute_expr(&1, locals)), locals) 61 | is_atom(head_result) -> compute_expr([head_result | tail], locals, true) 62 | true -> [head_result | compute_expr(tail, locals, true)] 63 | end 64 | end 65 | 66 | def compute_expr(ident, locals, _ret_locals) when is_atom(ident) do 67 | found = Map.get(locals, ident) 68 | 69 | if !found do 70 | raise Exceptions.UnknownLocalError, message: "unknown local `#{ident}`" 71 | end 72 | 73 | found 74 | end 75 | 76 | def compute_expr(node, locals, true), do: {node, locals} 77 | def compute_expr(node, _locals, _ret_locals), do: node 78 | 79 | defp check_tokens(tokens) do 80 | {lcount, rcount} = 81 | Enum.reduce(tokens, {0, 0}, fn 82 | {:"(", _}, {lcount, rcount} -> {lcount + 1, rcount} 83 | {:")", _}, {lcount, rcount} -> {lcount, rcount + 1} 84 | _, acc -> acc 85 | end) 86 | 87 | parens_balance = lcount - rcount 88 | 89 | # TODO: modify lexer to give column count, and enrich info here 90 | cond do 91 | parens_balance > 0 -> 92 | raise Exceptions.UnbalancedParensError, message: "missing one or more closing parentheses" 93 | 94 | parens_balance < 0 -> 95 | raise Exceptions.UnbalancedParensError, message: "missing one or more opening parentheses" 96 | 97 | true -> 98 | :ok 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/kamex/interpreter/builtins.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Interpreter.Builtins do 2 | @moduledoc false 3 | 4 | import Kamex.Interpreter, only: [compute_expr: 2] 5 | alias Kamex.Exceptions 6 | 7 | require __MODULE__.{Fold, Lists, Math} 8 | alias __MODULE__.{Fold, Lists, Math} 9 | 10 | @tru 1 11 | @fals 0 12 | @falsey [[], @fals] 13 | @num_tokens [:int, :float, :complex] 14 | 15 | @supported %{ 16 | println: :println, 17 | tack: :tack, 18 | nth: :nth, 19 | not: :not_, 20 | fac: :fac, 21 | map: :map, 22 | filter: :filter, 23 | count: :count, 24 | every: :every, 25 | "flat-map": :flat_map, 26 | # seq and id are functionally the same since we're eager eval 27 | id: :id, 28 | seq: :id, 29 | flatten: :flatten, 30 | discard: :discard, 31 | lift: :lift, 32 | type: :type, 33 | "to-string": :to_string, 34 | "parse-num": :parse_num, 35 | list_env: :list_env, 36 | size: :size, 37 | empty?: :empty? 38 | } 39 | 40 | defmacro mapping do 41 | map_supported_to_mod = fn mod, {call_name, fn_name} -> {call_name, {fn_name, mod}} end 42 | 43 | items = 44 | [ 45 | Enum.map(@supported, &map_supported_to_mod.(__MODULE__, &1)), 46 | Enum.map(Fold.supported(), &map_supported_to_mod.(Fold, &1)), 47 | Enum.map(Lists.supported(), &map_supported_to_mod.(Lists, &1)), 48 | Enum.map(Math.supported(), &map_supported_to_mod.(Math, &1)) 49 | ] 50 | |> Enum.map(&Enum.into(&1, %{})) 51 | |> Enum.reduce(%{}, &Map.merge/2) 52 | 53 | Macro.escape(items) 54 | end 55 | 56 | def builtin?(name), do: Map.get(mapping(), name) 57 | 58 | def run(name, args, locals) do 59 | args = Enum.map(args, &compute_expr(&1, locals)) 60 | {real_fn, mod} = Map.get(mapping(), name) 61 | 62 | {apply(mod, real_fn, [args, locals]), locals} 63 | end 64 | 65 | # --------------------------------------------------------------------------- 66 | 67 | def println([term], _) do 68 | IO.puts(term) 69 | term 70 | end 71 | 72 | def tack([nth], _) do 73 | fn args_to_pick, _locals -> Enum.at(args_to_pick, nth) end 74 | end 75 | 76 | def nth([n, args], _), do: Enum.at(args, n) 77 | 78 | # def not_([value], _), do: if(value in @falsey, do: @tru, else: @fals) 79 | def not_([x], _) when x in @falsey, do: @tru 80 | def not_([_], _), do: @fals 81 | 82 | def fac([0], _), do: 1 83 | def fac([num], _) when is_integer(num), do: num * fac([num - 1], nil) 84 | 85 | def map([fun, arg], locals) when is_list(arg), do: map([fun | arg], locals) 86 | def map([fun, str], locals) when is_binary(str), do: map([fun | String.codepoints(str)], locals) 87 | def map([fun | args], locals), do: Enum.map(args, &compute_expr([fun, &1], locals)) 88 | 89 | def filter([fun, list], locals), 90 | # Compare against 0 so that we don't have to do a second `not` check for `1`. 91 | do: Enum.filter(list, fn item -> not_([compute_expr([fun, item], locals)], nil) == 0 end) 92 | 93 | def count([fun, list], locals), do: Enum.count(filter([fun, list], locals)) 94 | 95 | def every([fun, list], locals), 96 | do: 97 | if(Enum.all?(list, fn item -> not_([compute_expr([fun, item], locals)], nil) == 0 end), 98 | do: @tru, 99 | else: @fals 100 | ) 101 | 102 | def flat_map([fun, args], locals), do: Enum.flat_map(args, &compute_expr([fun, &1], locals)) 103 | 104 | def id([x], _), do: x 105 | 106 | def flatten([l], _) when is_list(l), do: List.flatten(l) 107 | 108 | def discard([], _), do: [] 109 | 110 | def lift([c, l], locals) when is_list(l), do: compute_expr([c | l], locals) 111 | 112 | # TODO: iterate, scanterate 113 | # def iterate(n, c, f | rest], _) when is_number(n) do 114 | # rest = [f | rest] 115 | 116 | # end 117 | 118 | # TODO: need to better differentiate between callable/identifier, as atoms can be both. See what kamilalisp does 119 | def type([x], _) when is_list(x), do: "list" 120 | def type([x], _) when is_binary(x), do: "string" 121 | def type([x], _) when is_integer(x), do: "integer" 122 | def type([x], _) when is_number(x), do: "real" 123 | def type([x], _) when is_atom(x), do: "identifier" 124 | def type([x], _) when is_function(x), do: "callable" 125 | def type([%Complex{}], _), do: "complex" 126 | def type([%Tensor.Tensor{}], _), do: "matrix" 127 | 128 | def to_string([x], _), do: to_string(x) 129 | 130 | def parse_num([str], _) when is_binary(str) do 131 | with {:ok, [token], _} <- :lexer.string(String.to_charlist(str)), 132 | {type, _, _} when type in @num_tokens <- token do 133 | Kamex.Parser.parse(token) 134 | else 135 | _ -> 136 | raise Exceptions.IllegalTypeError, message: "parse_num: expected valid number string" 137 | end 138 | end 139 | 140 | # TODO: eval, memo, parse, dyad, monad 141 | 142 | def list_env([], locals), do: Map.keys(locals) 143 | 144 | # TODO: import 145 | 146 | def size([str], _) when is_binary(str), do: String.length(str) 147 | def size([l], _) when is_list(l), do: length(l) 148 | 149 | def empty?([x], _) when x == [] or x == "", do: true 150 | def empty?([x], _) when is_list(x) or is_binary(x), do: false 151 | 152 | # TODO: cmp-ex is gonna be a doozy 153 | end 154 | -------------------------------------------------------------------------------- /lib/kamex/interpreter/builtins/fold.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Interpreter.Builtins.Fold do 2 | @moduledoc false 3 | 4 | import Kamex.Interpreter, only: [compute_expr: 2] 5 | alias Kamex.Exceptions 6 | 7 | defmacro supported do 8 | quote do 9 | %{ 10 | foldl: :foldl, 11 | foldr: :foldr, 12 | foldl1: :foldl1, 13 | foldr1: :foldr1, 14 | scanl: :scanl, 15 | scanr: :scanr, 16 | scanl1: :scanl1, 17 | scanr1: :scanr1 18 | } 19 | end 20 | end 21 | 22 | def foldl([_f, acc, []], _), do: acc 23 | 24 | def foldl([f, acc, data], locals) when is_list(data), 25 | do: List.foldl(data, acc, &compute_expr([f, &2, &1], locals)) 26 | 27 | # TODO: supports 2-ary to become -1 varient? 28 | 29 | def foldr([_f, acc, []], _), do: acc 30 | 31 | # For some reason the -r functions pass acc as second. 32 | def foldr([f, acc, data], locals) when is_list(data), 33 | do: List.foldr(data, acc, &compute_expr([f, &1, &2], locals)) 34 | 35 | def foldl1([_f, []], _), 36 | do: raise(Exceptions.IllegalTypeError, message: "foldl1: cannot fold empty list") 37 | 38 | def foldl1([_f, [x]], _), do: x 39 | 40 | def foldl1([f, [head | tail]], locals), 41 | do: List.foldl(tail, head, &compute_expr([f, &2, &1], locals)) 42 | 43 | def foldr1([_f, []], _), 44 | do: raise(Exceptions.IllegalTypeError, message: "foldr1: cannot fold empty list") 45 | 46 | def foldr1([_f, [x]], _), do: x 47 | 48 | def foldr1([f, [head | tail]], locals), 49 | do: List.foldr(tail, head, &compute_expr([f, &1, &2], locals)) 50 | 51 | # --- 52 | # scanl, scanr 53 | 54 | def scanl([_f, acc, []], _), do: [acc] 55 | 56 | def scanl([f, acc, data], locals) when is_list(data), 57 | do: Enum.scan(data, acc, &compute_expr([f, &2, &1], locals)) 58 | 59 | def scanr([_f, acc, []], _), do: [acc] 60 | 61 | def scanr([f, acc, data], locals) when is_list(data), 62 | do: Enum.scan(Enum.reverse(data), acc, &compute_expr([f, &1, &2], locals)) |> Enum.reverse() 63 | 64 | def scanl1([_f, []], _), 65 | do: raise(Exceptions.IllegalTypeError, message: "scanl1: cannot scan empty list") 66 | 67 | def scanl1([_f, [x]], _), do: [x] 68 | 69 | def scanl1([f, list], locals), 70 | do: Enum.scan(list, &compute_expr([f, &2, &1], locals)) 71 | 72 | def scanr1([_f, []], _), 73 | do: raise(Exceptions.IllegalTypeError, message: "scanr1: cannot scan empty list") 74 | 75 | def scanr1([_f, [x]], _), do: [x] 76 | 77 | def scanr1([f, list], locals), 78 | do: Enum.scan(Enum.reverse(list), &compute_expr([f, &1, &2], locals)) |> Enum.reverse() 79 | end 80 | -------------------------------------------------------------------------------- /lib/kamex/interpreter/builtins/lists.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Interpreter.Builtins.Lists do 2 | @moduledoc false 3 | 4 | import Kamex.Interpreter, only: [compute_expr: 2] 5 | alias Kamex.{Exceptions, Interpreter.Builtins, Util.Comb} 6 | alias Tensor.{Matrix, Tensor} 7 | 8 | @tru 1 9 | @fals 0 10 | 11 | defmacro supported do 12 | quote do 13 | %{ 14 | iota: :iota, 15 | tie: :tie, 16 | flatten: :flatten, 17 | reverse: :reverse, 18 | rotate: :rotate, 19 | zip: :zip, 20 | first: :first, 21 | "first-idx": :first_idx, 22 | cons: :cons, 23 | append: :append, 24 | car: :car, 25 | cdr: :cdr, 26 | cadr: :cadr, 27 | "str-split": :str_split, 28 | size: :size, 29 | any: :any, 30 | "grade-up": :grade_up, 31 | "grade-down": :grade_down, 32 | index: :index, 33 | at: :at, 34 | unique: :unique, 35 | where: :where, 36 | replicate: :replicate, 37 | drop: :drop, 38 | intersperse: :intersperse, 39 | "unique-mask": :unique_mask, 40 | prefixes: :prefixes, 41 | suffixes: :suffixes, 42 | partition: :partition, 43 | window: :window, 44 | "inner-prod": :inner_prod, 45 | "outer-prod": :outer_prod, 46 | range: :range, 47 | "starts-with": :starts_with, 48 | keys: :keys, 49 | "index-of": :index_of, 50 | ucs: :ucs, 51 | in?: :in?, 52 | "find-seq": :find_seq, 53 | shuffle: :shuffle, 54 | "str-join": :str_join, 55 | "str-explode": :str_explode 56 | } 57 | end 58 | end 59 | 60 | # TODO: fix stuff involving lists so that we can lazy this and make big iotas nicer to work with? 61 | def iota([x], _) when is_integer(x), do: 0..(x - 1) |> Enum.into([]) 62 | 63 | def iota([x], _) when is_list(x) do 64 | iotas = 65 | Enum.map(x, fn 66 | n when is_integer(n) -> iota([n], nil) 67 | _ -> raise Exceptions.IllegalTypeError, message: "iota: expected a list of integers" 68 | end) 69 | 70 | Comb.cartesian_product(iotas) 71 | # iotas 72 | # |> Enum.reduce(&Comb.cart_product([&1, &2])) 73 | # |> Enum.map(fn x -> x |> List.flatten() |> Enum.reverse() end) 74 | end 75 | 76 | def tie(args, _), do: args 77 | 78 | def flatten([list], _) when is_list(list), do: List.flatten(list) 79 | 80 | def reverse([str], _) when is_binary(str), do: String.reverse(str) 81 | def reverse([list], _) when is_list(list), do: Enum.reverse(list) 82 | 83 | # Cycle items through the list n times, kinda like bitshift but wrapping around 84 | # (rotate 3 '(1 2 3 4 5 6 7 8 9)) 85 | # -> '(4 5 6 7 8 9 1 2 3) 86 | def rotate([0, list], _) when is_list(list), do: list 87 | 88 | def rotate([n, list], _) when abs(n) > length(list), 89 | do: rotate([rem(n, length(list)), list], nil) 90 | 91 | def rotate([n, list], _) when is_list(list) do 92 | {back, front} = Enum.split(list, n) 93 | front ++ back 94 | end 95 | 96 | def zip([x, y], _) when is_list(x) and is_list(y), do: Enum.zip(x, y) 97 | 98 | def first([fun, list], locals) when is_list(list), 99 | do: 100 | Enum.find(list, fn item -> Builtins.not_([compute_expr([fun, item], locals)], nil) == 0 end) 101 | 102 | def first_idx([fun, list], locals) when is_list(list), 103 | do: 104 | list 105 | |> Enum.with_index() 106 | |> Enum.find({nil, nil}, fn {item, _} -> 107 | Builtins.not_([compute_expr([fun, item], locals)], nil) == 0 108 | end) 109 | |> elem(1) 110 | 111 | def cons([head, tail], _) when is_list(tail), do: [head | tail] 112 | 113 | def cons([_head, _tail], _), 114 | do: raise(Exceptions.IllegalTypeError, message: "cons: second argument must be a list") 115 | 116 | # TOO: better error for when first arg is non list/check to make sure all items is a list (how without iterating through all items?) 117 | def append(lists, _), do: Enum.reduce(lists, [], fn x, acc -> acc ++ x end) 118 | 119 | def car([[head | _]], _), do: head 120 | def cdr([[_ | tail]], _), do: tail 121 | def cadr([[_, head2 | _]], _), do: head2 122 | 123 | def str_split([str, ""], _) when is_binary(str), 124 | do: str |> String.split("") |> Enum.slice(1..-1) 125 | 126 | def str_split([str, delim], _) when is_binary(str) and is_binary(delim), 127 | do: String.split(str, delim) 128 | 129 | def size([str], _) when is_binary(str), do: String.length(str) 130 | def size([list], _) when is_list(list), do: length(list) 131 | 132 | def any([fun, list], locals) when is_list(list), 133 | do: 134 | Enum.any?(list, fn item -> Builtins.not_([compute_expr([fun, item], locals)], nil) == 0 end) 135 | 136 | # TODO: sort 137 | 138 | # Return a list of indicies which when followed, result in a sorted list. 139 | def grade_up([list], _) when is_list(list), 140 | do: 141 | list 142 | |> Enum.with_index() 143 | |> Enum.sort(fn {x, _}, {y, _} -> x < y end) 144 | |> Enum.map(&elem(&1, 1)) 145 | 146 | def grade_up([fun, list], locals) when is_list(list), 147 | do: 148 | list 149 | |> Enum.with_index() 150 | |> Enum.sort(fn {x, _}, {y, _} -> 151 | Builtins.not_([compute_expr([fun, x, y], locals)], nil) == 0 152 | end) 153 | |> Enum.map(&elem(&1, 1)) 154 | 155 | def grade_down([list], _) when is_list(list), do: grade_up([list], nil) |> Enum.reverse() 156 | 157 | def grade_down([fun, list], locals) when is_list(list), 158 | do: grade_up([fun, list], locals) |> Enum.reverse() 159 | 160 | def index([indices, list], _) when is_list(indices) and is_list(list), 161 | # TODO: handle out of bound indices? 162 | do: Enum.map(indices, fn i -> Enum.at(list, i) end) 163 | 164 | # '*'@(2∘|) 1 2 3 4 5 165 | # * 2 * 4 * 166 | def at([transform, indices, list], locals) when is_list(indices) and is_list(list), 167 | # TODO: check list is all numbers 168 | do: 169 | list 170 | |> Enum.with_index() 171 | |> Enum.map(fn {item, i} -> 172 | if i in indices, 173 | do: compute_expr([transform, item], locals), 174 | else: item 175 | end) 176 | 177 | def at([transform, pred, list], locals) when is_list(list), 178 | do: 179 | Enum.map(list, fn item -> 180 | pick_it = Builtins.not_([compute_expr([pred, item], locals)], nil) == 0 181 | 182 | if pick_it, 183 | do: compute_expr([transform, item], locals), 184 | else: item 185 | end) 186 | 187 | def unique([str], _) when is_binary(str), 188 | do: str |> String.codepoints() |> Enum.uniq() |> Enum.join() 189 | 190 | def unique([list], _) when is_list(list), do: Enum.uniq(list) 191 | 192 | # where: indicies of true items in a bool vector, > 1 returns indice n times 193 | # ⍸1 0 1 0 3 1 194 | # 1 3 5 5 5 6 195 | def where([vector], _), 196 | do: 197 | vector 198 | |> Stream.with_index() 199 | |> Stream.map(fn {x, i} -> 200 | case x do 201 | 0 -> 0 202 | 1 -> i 203 | _ -> List.duplicate(i, x) 204 | end 205 | end) 206 | |> Enum.into([]) 207 | |> List.flatten() 208 | 209 | # replicate: apply a bool vector mask to an list, repeat like `where` if > 1 210 | # 0 1 0 1/2 3 4 5 211 | # 3 5 212 | def replicate([times, list], _) when is_integer(times) and is_list(list), 213 | do: replicate([List.duplicate(times, length(list)), list], nil) 214 | 215 | def replicate([mask, list], _) when is_list(mask) and is_list(list), 216 | do: 217 | Enum.zip(mask, list) 218 | |> Enum.filter(fn {m, _} -> m != 0 end) 219 | |> Enum.map(fn {times, item} -> List.duplicate(item, times) end) 220 | |> Enum.into([]) 221 | |> List.flatten() 222 | 223 | def drop([0, list], _) when is_list(list), do: list 224 | def drop([n, list], _) when is_list(list) and n < 0, do: list |> Enum.drop(-n) 225 | def drop([n, list], _) when is_list(list) and n > 0, do: list |> Enum.drop(n) 226 | 227 | def intersperse([l, r], _), do: Enum.zip(l, r) |> Enum.flat_map(fn {x, y} -> [x, y] end) 228 | 229 | # unique mask aka nub sieve. 230 | # get a boolean vector indicating which first instance of each element in al ist 231 | def unique_mask([str], _) when is_binary(str), do: unique_mask([String.codepoints(str)], nil) 232 | 233 | def unique_mask([list], _) when is_list(list), 234 | do: 235 | Enum.reduce(list, {[], []}, fn curr, {seen, vector} -> 236 | if curr in seen, 237 | do: {seen, [@fals | vector]}, 238 | else: {[curr | seen], [@tru | vector]} 239 | end) 240 | |> elem(1) 241 | |> Enum.reverse() 242 | 243 | # (prefixes "example") 244 | # ["e", "ex", "exa", "exam", "examp", "exampl", "example"] 245 | def prefixes([str], _) when is_binary(str), 246 | do: str |> String.codepoints() |> Enum.scan(&(&2 <> &1)) 247 | 248 | def prefixes([list], _) when is_list(list), 249 | do: 1..length(list) |> Enum.map(&Enum.slice(list, 0, &1)) 250 | 251 | # inverse of `prefixes`, i.e. starts from the end and works backwards 252 | # (suffixes "example") 253 | # ["e", "le", "ple", "mple", "ample", "xample", "example"] 254 | def suffixes([str], _) when is_binary(str), 255 | do: str |> String.codepoints() |> Enum.reverse() |> Enum.scan(&(&1 <> &2)) 256 | 257 | def suffixes([list], _) when is_list(list) do 258 | len = length(list) 259 | 1..len |> Enum.map(&Enum.slice(list, -&1, len)) 260 | end 261 | 262 | def partition([vector, str], _) when is_list(vector) and is_binary(str), 263 | do: partition([vector, String.codepoints(str)], nil) |> Enum.map(&Enum.join/1) 264 | 265 | def partition([vector, list], _) 266 | when is_list(vector) and is_list(list) and length(vector) == length(list) do 267 | # TODO: find a more efficient way of detecting boolean vectors 268 | bool_vector = 269 | !Enum.find(vector, fn 270 | 1 -> false 271 | 0 -> false 272 | _ -> true 273 | end) 274 | 275 | zipped = Enum.zip(vector, list) 276 | 277 | zipped 278 | |> then( 279 | &if bool_vector do 280 | Enum.reduce(&1, {0, [[]]}, fn 281 | # If current is 0, just return 0 and result 282 | {0, _}, {_, result} -> {0, result} 283 | # If current is 1 and previous is 0, start a new string 284 | {1, curr}, {0, result} -> {1, [[curr] | result]} 285 | # Otherwise appedn to the current string 286 | {1, curr}, {1, [curr_list | rest]} -> {1, [[curr | curr_list] | rest]} 287 | end) 288 | else 289 | Enum.reduce(&1, {0, [[]]}, fn 290 | # Discard any 0s 291 | {0, _}, acc -> 292 | acc 293 | 294 | # If the current vector value is smaller or bigger than the current biggest, append to current string. 295 | {val, curr}, {biggest, [curr_list | rest]} when val <= biggest -> 296 | {val, [[curr | curr_list] | rest]} 297 | 298 | # Else if it's the new biggest, start a new string. 299 | {val, curr}, {_, result} -> 300 | {val, [[curr] | result]} 301 | end) 302 | end 303 | ) 304 | |> elem(1) 305 | |> Enum.reverse() 306 | |> Enum.map(&Enum.reverse/1) 307 | # Remove empty list at the start. Need to figure out why it happens 308 | |> Enum.slice(1..-1) 309 | end 310 | 311 | # just a sliding window on a list 312 | # (window 2 "example") 313 | # ["ex", "xa", "am", "mp", "pl", "le"] 314 | def window([size, str], _) when is_binary(str) and size > 0, 315 | do: 316 | str 317 | |> String.codepoints() 318 | |> Enum.chunk_every(size, 1, :discard) 319 | |> Enum.reduce([], &[Enum.join(&1) | &2]) 320 | |> Enum.reverse() 321 | 322 | def window([size, list], _) when is_list(list) and size > 0, 323 | do: Enum.chunk_every(list, size, 1, :discard) 324 | 325 | # TODO: enforce callables here and in other functions like map 326 | def inner_prod([_f, _g, [], []], _), do: [] 327 | def inner_prod([_f, g, [x], [y]], locals), do: compute_expr([g, x, y], locals) 328 | 329 | def inner_prod([f, g, l1, l2], locals) 330 | when is_list(l1) and is_list(l2) and length(l1) == length(l2) do 331 | Enum.zip_with(l1, l2, &compute_expr([g, &1, &2], locals)) 332 | |> Enum.reduce(&compute_expr([f, &1, &2], locals)) 333 | end 334 | 335 | def inner_prod([f, g, %Tensor{} = a, %Tensor{} = b], locals) do 336 | [n_a_rows, n_a_cols] = Tensor.dimensions(a) 337 | [n_b_rows, n_b_cols] = Tensor.dimensions(b) 338 | 339 | if n_a_rows != n_b_cols do 340 | raise ArgumentError, 341 | message: 342 | "inner_prod: invalid matrices: #{n_a_rows}x#{n_a_cols} and #{n_b_rows}x#{n_b_cols}" 343 | end 344 | 345 | rows = Matrix.rows(a) 346 | cols = Matrix.columns(b) 347 | xn = length(rows) - 1 348 | yn = length(cols) - 1 349 | 350 | result = 351 | for x <- 0..xn, 352 | y <- 0..yn, 353 | do: 354 | Enum.zip_with( 355 | Enum.at(rows, x), 356 | Enum.at(cols, y), 357 | &compute_expr([g, [&1, &2]], locals) 358 | ) 359 | |> Enum.reduce(&compute_expr([f, [&1, &2]], locals)) 360 | 361 | result 362 | |> Enum.chunk_every(n_b_cols) 363 | |> then(&Matrix.new(&1, n_a_rows, n_b_cols)) 364 | end 365 | 366 | def outer_prod([f, a, b], locals) when is_list(a) and is_list(b), 367 | do: Comb.cartesian_product([a, b]) |> Enum.map(&compute_expr([f, &1], locals)) 368 | 369 | # is this inclusive? 370 | def range([start, stop], _) when is_integer(start) and is_integer(stop), do: start..stop 371 | 372 | def starts_with([prefix, string], _) when is_binary(string) and is_binary(prefix), 373 | do: if(String.starts_with?(string, prefix), do: @tru, else: @fals) 374 | 375 | def starts_with([prefix, list], _) when is_list(list) and is_list(prefix), 376 | do: if(List.starts_with?(list, prefix), do: @tru, else: @fals) 377 | 378 | # --> (keys "Mississippi") 379 | # (("M" (0)) ("i" (1 4 7 10)) ("s" (2 3 5 6)) ("p" (8 9))) 380 | def keys([str], _) when is_binary(str), 381 | do: keys([String.codepoints(str)], nil) 382 | 383 | def keys([list], _) when is_list(list), 384 | do: 385 | list 386 | |> Enum.with_index() 387 | |> Enum.reduce(%{}, fn {v, i}, map -> Map.put(map, v, [i | Map.get(map, v, [])]) end) 388 | |> Enum.map(fn {k, v} -> [k, Enum.reverse(v)] end) 389 | 390 | def index_of([to_check, str], _) when is_binary(to_check) and is_binary(str), 391 | do: index_of([String.codepoints(to_check), String.codepoints(str)], nil) 392 | 393 | def index_of([to_check, list], _) when is_list(to_check) and is_list(list), 394 | do: Enum.map(to_check, fn x -> Enum.find_index(list, x) end) 395 | 396 | # turn unicode chars to ints and vice versa 397 | # (ucs "Hello World") 398 | # [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100] 399 | def ucs([str], _) when is_binary(str), 400 | do: :binary.bin_to_list(str) 401 | 402 | def ucs([list], _) when is_list(list), do: Enum.into(list, <<>>, &<<&1>>) 403 | 404 | def in?([needle, haystack]) when is_binary(needle) and is_binary(haystack), 405 | do: if(String.contains?(haystack, needle), do: @tru, else: @fals) 406 | 407 | def in?([needle, haystack]) when is_list(haystack), 408 | do: if(needle in haystack, do: @tru, else: @fals) 409 | 410 | def find_seq([needle, haystack], _) when is_binary(needle) and is_binary(haystack), 411 | do: in?([needle, haystack]) 412 | 413 | def find_seq([needle, haystack], _) when is_list(needle) and is_list(haystack), 414 | do: if(Kamex.Util.sublist?(haystack, needle), do: @tru, else: @fals) 415 | 416 | def shuffle([list], _) when is_list(list), do: Enum.shuffle(list) 417 | 418 | def str_join([list], _) when is_list(list), do: Enum.join(list) 419 | def str_explode([str], _) when is_binary(str), do: str |> String.split() |> Enum.slice(1..-1) 420 | end 421 | -------------------------------------------------------------------------------- /lib/kamex/interpreter/builtins/math.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Interpreter.Builtins.Math do 2 | @moduledoc false 3 | alias Kamex.Exceptions 4 | alias Kamex.Util.Math, as: Kamath 5 | 6 | @tru 1 7 | @fals 0 8 | # @falsey [[], @fals] 9 | 10 | # Go home diaz ur drunk 11 | @dialyzer {:nowarn_function, nth_root: 2} 12 | 13 | defmacro supported do 14 | quote do 15 | %{ 16 | +: :add, 17 | -: :subtract, 18 | *: :multiply, 19 | /: :divide, 20 | "//": :divide_int, 21 | %: :modulo, 22 | sqrt: :sqrt, 23 | "nth-root": :nth_root, 24 | **: :power, 25 | =: :equals, 26 | "/=": :not_equals, 27 | <: :less_than, 28 | >: :greater_than, 29 | <=: :less_than_or_equal, 30 | >=: :greater_than_or_equal, 31 | ceil: :ceiling, 32 | floor: :floor, 33 | gcd: :gcd, 34 | lcm: :lcm, 35 | bernoulli: :bernoulli, 36 | digamma: :digamma, 37 | "lambert-w0": :lambert_w0, 38 | "jacobi-sym": :jacobi_sym, 39 | exp: :exp, 40 | "even-f": :even_f, 41 | "odd-f": :odd_f, 42 | odd?: :odd, 43 | even?: :even, 44 | min: :min_, 45 | max: :max_, 46 | "hamming-weight": :hamming_weight, 47 | re: :re, 48 | im: :im, 49 | phasor: :phasor, 50 | "as-complex": :as_complex, 51 | "as-real": :as_real 52 | } 53 | end 54 | end 55 | 56 | def add([a, b], _) when is_binary(a) and is_binary(b), do: a <> b 57 | def add([a, b], _) when is_number(a) and is_number(b), do: a + b 58 | # Can't do my own `is_complex` helper so multiple clauses will have to do :/ 59 | def add([%Complex{} = a, %Complex{} = b], _), do: Complex.add(a, b) 60 | def add([%Complex{} = a, b], _) when is_number(b), do: Complex.add(a, b) 61 | def add([a, %Complex{} = b], _) when is_number(a), do: Complex.add(a, b) 62 | 63 | def add([a, b], _) when is_number(a) and is_binary(b), do: to_string(a) <> b 64 | def add([a, b], _) when is_binary(a) and is_number(b), do: a <> to_string(b) 65 | def add([%Complex{} = a, b]) when is_binary(b), do: to_string(a) <> b 66 | def add([a, %Complex{} = b]) when is_binary(a), do: a <> to_string(b) 67 | # TODO: better errors for mismatching types 68 | 69 | # --- 70 | 71 | def subtract([x], _) when is_number(x), do: -x 72 | def subtract([%Complex{} = x], _), do: Complex.neg(x) 73 | 74 | def subtract([a, b], _) when is_number(a) and is_number(b), do: a - b 75 | def subtract([%Complex{} = a, %Complex{} = b], _), do: Complex.sub(a, b) 76 | def subtract([%Complex{} = a, b], _) when is_number(b), do: Complex.sub(a, b) 77 | def subtract([a, %Complex{} = b], _) when is_number(a), do: Complex.sub(a, b) 78 | 79 | # --- 80 | # TODO: ask how unary multiply works 81 | 82 | def multiply([a, b], _) when is_number(a) and is_number(b), do: a * b 83 | def multiply([%Complex{} = a, %Complex{} = b], _), do: Complex.mult(a, b) 84 | def multiply([%Complex{} = a, b], _) when is_number(b), do: Complex.mult(a, b) 85 | def multiply([a, %Complex{} = b], _) when is_number(a), do: Complex.mult(a, b) 86 | 87 | # --- 88 | # TODO: ask how unary division works 89 | 90 | def divide([a, b], _) when is_number(a) and is_number(b), do: a / b 91 | def divide([%Complex{} = a, %Complex{} = b], _), do: Complex.div(a, b) 92 | def divide([%Complex{} = a, b], _) when is_number(b), do: Complex.div(a, Complex.new(b)) 93 | def divide([a, %Complex{} = b], _) when is_number(a), do: Complex.div(Complex.new(a), b) 94 | 95 | # --- 96 | # TODO: integer division 97 | 98 | # --- 99 | 100 | def modulo([x], _) when is_number(x), do: abs(x) 101 | def modulo([%Complex{} = x], _), do: Complex.sqrt(Kamath.norm_complex(x)) 102 | def modulo([a, b], _) when is_integer(a) and is_integer(b), do: rem(a, b) 103 | 104 | def sqrt([x], _) when is_number(x), do: Math.sqrt(x) 105 | 106 | def sqrt([%Complex{im: 0, re: re}], _) when re < 0, 107 | do: raise(Exceptions.MathError, message: "sqrt: im=0 re<0 doesn't exist") 108 | 109 | # TODO: pala's impl is different, compare 110 | def sqrt([%Complex{} = x], _), do: Complex.sqrt(x) 111 | 112 | # --- 113 | 114 | def nth_root([_, exp], _) when not is_number(exp), 115 | do: raise(Exceptions.IllegalTypeError, message: "nth_root: exponent must be a real number") 116 | 117 | # TODO: need to convert to int, which way does kami round? 118 | def nth_root([x, exp], _) when is_integer(x), do: trunc(x ** (1 / exp)) 119 | def nth_root([x, exp], _) when is_float(x), do: x ** (1 / exp) 120 | 121 | def nth_root([%Complex{} = x, exp], _), 122 | do: Complex.pow(x, Complex.new(1 / exp)) 123 | 124 | # --- 125 | 126 | def power([x, exp], _) when is_number(x) and is_number(exp), do: :math.pow(x, exp) 127 | def power([%Complex{} = x, exp], _) when is_number(exp), do: Complex.pow(x, Complex.new(exp)) 128 | 129 | # TODO: move iota here, and support 3-arg for it 130 | # TODO: see if putting comparisons in guards is faster than if/else 131 | 132 | def equals([a, b], _) when a == b, do: @tru 133 | def equals([_, _], _), do: @fals 134 | 135 | def not_equals([a, b], _) when a == b, do: @fals 136 | def not_equals([_, _], _), do: @tru 137 | 138 | def less_than([a, b], _) when a < b, do: @tru 139 | def less_than([_, _], _), do: @fals 140 | def less_than([x], _) when is_number(x), do: x - 1 141 | def less_than([%Complex{} = x], _), do: Complex.sub(x, 1) 142 | # TODO: unary < string works like ruby pred, same for > and succ. 143 | 144 | def greater_than([a, b], _) when a > b, do: @tru 145 | def greater_than([_, _], _), do: @fals 146 | def greater_than([x], _) when is_number(x), do: x + 1 147 | def greater_than([%Complex{} = x], _), do: Complex.add(x, 1) 148 | 149 | def less_equal([a, b], _) when a <= b, do: @tru 150 | def less_equal([_, _], _), do: @fals 151 | 152 | def greater_equal([a, b], _) when a >= b, do: @tru 153 | def greater_equal([_, _], _), do: @fals 154 | 155 | # --- 156 | 157 | def ceiling([x], _) when is_number(x), do: :math.ceil(x) 158 | def ceiling([%Complex{im: im, re: re}], _), do: Complex.new(:math.ceil(re), :math.ceil(im)) 159 | def ceiling([x, 0], _), do: ceiling([x], nil) 160 | 161 | def ceiling([x, places], _) when is_number(x) and is_integer(places) and places > 0, 162 | do: :math.ceil(x * 10 ** places) / 10 ** places 163 | 164 | # TODO: explain what the fuck, can probably just call with abs 165 | def ceiling([x, places], _) when is_number(x) and is_integer(places) and places < 0, 166 | do: :math.ceil(x / 10 ** places) * 10 ** places 167 | 168 | def ceiling([%Complex{im: im, re: re}, places], _), 169 | do: Complex.new(ceiling([re, places], nil), ceiling([im, places], nil)) 170 | 171 | def floor([x], _) when is_number(x), do: :math.floor(x) 172 | def floor([%Complex{im: im, re: re}], _), do: Complex.new(:math.floor(re), :math.floor(im)) 173 | def floor([x, 0], _), do: floor([x], nil) 174 | 175 | def floor([x, places], _) when is_number(x) and is_integer(places) and places > 0, 176 | do: :math.floor(x * 10 ** places) / 10 ** places 177 | 178 | def floor([x, places], _) when is_number(x) and is_integer(places) and places < 0, 179 | do: :math.floor(x / 10 ** places) * 10 ** places 180 | 181 | def floor([%Complex{im: im, re: re}, places], _), 182 | do: Complex.new(floor([re, places], nil), floor([im, places], nil)) 183 | 184 | # --- 185 | # TODO: binary or, and, xor 186 | 187 | def gcd([a, b], _) when is_integer(a) and is_integer(b), do: Math.gcd(a, b) 188 | 189 | def gcd([n1, n2], _) 190 | when (is_integer(n1) and is_float(n2)) or (is_float(n1) and is_integer(n2)) or 191 | (is_float(n1) and is_float(n2)) do 192 | # gcd(a/b, c/d) = gcd(a, c) / lcm(b, d) 193 | %{numerator: a, denominator: b} = Ratio.FloatConversion.float_to_rational(n1) 194 | %{numerator: c, denominator: d} = Ratio.FloatConversion.float_to_rational(n2) 195 | top = Math.gcd(a, c) 196 | bottom = Math.lcm(b, d) 197 | 198 | # Would normally return a fraction but its not a core type so lol nah 199 | top / bottom 200 | end 201 | 202 | def gcd([%Complex{} = n1, n2], _) when is_integer(n2), 203 | do: Kamath.gcd_complex(n1, Complex.new(n2)) 204 | 205 | def gcd([n1, %Complex{} = n2], _) when is_integer(n1), 206 | do: Kamath.gcd_complex(Complex.new(n1), n2) 207 | 208 | def gcd([%Complex{} = n1, %Complex{} = n2], _), do: Kamath.gcd_complex(n1, n2) 209 | 210 | # --- 211 | def lcm([a, b], _) when is_integer(a) and is_integer(b), do: Math.lcm(a, b) 212 | 213 | # Basically just the inverse of the float GCM 214 | def lcm([n1, n2], _) 215 | when (is_integer(n1) and is_float(n2)) or (is_float(n1) and is_integer(n2)) or 216 | (is_float(n1) and is_float(n2)) do 217 | # lcm/b, c/d) = lcm(a, c) / gcd(b, d) 218 | %{numerator: a, denominator: b} = Ratio.FloatConversion.float_to_rational(n1) 219 | %{numerator: c, denominator: d} = Ratio.FloatConversion.float_to_rational(n2) 220 | top = Math.lcm(a, c) 221 | bottom = Math.gcd(b, d) 222 | 223 | # Would normally return a fraction but its not a core type so lol nah 224 | top / bottom 225 | end 226 | 227 | # Alternatively `(c1 * c2) / gcd_complex(c1, c2)` 228 | def lcm([%Complex{} = n1, n2], _) when is_integer(n2), 229 | do: Kamath.lcm_complex(n1, Complex.new(n2)) 230 | 231 | def lcm([n1, %Complex{} = n2], _) when is_integer(n1), 232 | do: Kamath.lcm_complex(Complex.new(n1), n2) 233 | 234 | def lcm([%Complex{} = n1, %Complex{} = n2], _), do: Kamath.lcm_complex(n1, n2) 235 | 236 | # --- 237 | 238 | def bernoulli([x], _) when is_integer(x), do: Kamath.bernoulli(x) 239 | 240 | def digamma([x], _) when is_number(x), do: digamma([Complex.new(x)], nil) 241 | 242 | def digamma([%Complex{} = x], nil) do 243 | # phi(z) = ln(z) - 1/2z 244 | z = Complex.sub(Complex.ln(x), Complex.div(Complex.new(1), Complex.mult(2, x))) 245 | if z.im != 0, do: z, else: z.re 246 | end 247 | 248 | def lambert_w0([x], _) when is_number(x), do: Kamath.lambert_w(x) 249 | def lambert_w0([%Complex{re: x}], _), do: Kamath.lambert_w(x) 250 | 251 | def jacobi_sym([n, k], _) when is_integer(n) and is_integer(k), do: Kamath.jacobi(n, k) 252 | 253 | # --- 254 | 255 | def exp([x], _) when is_number(x), do: Math.exp(x) 256 | def exp([%Complex{} = x], _), do: Complex.exp(x) 257 | 258 | def even_f([x], _) when is_number(x), do: x * 2 259 | def odd_f([x], _) when is_number(x), do: x * 2 + 1 260 | 261 | def odd([x], _) when is_integer(x), do: if(rem(x, 2) == 1, do: @tru, else: @fals) 262 | def even([x], _) when is_integer(x), do: if(rem(x, 2) == 0, do: @tru, else: @fals) 263 | 264 | # min/max 265 | def min_([a, b], _) when is_number(a) and is_number(b), do: min(a, b) 266 | def min_([%Complex{} = a, %Complex{} = b], _), do: Kamath.min_complex(a, b) 267 | def min_([%Complex{} = a, b], _) when is_number(b), do: Kamath.min_complex(a, Complex.new(b)) 268 | def min_([a, %Complex{} = b], _) when is_number(a), do: Kamath.min_complex(Complex.new(a), b) 269 | 270 | def max_([a, b], _) when is_number(a) and is_number(b), do: max(a, b) 271 | def max_([%Complex{} = a, %Complex{} = b], _), do: Kamath.max_complex(a, b) 272 | def max_([%Complex{} = a, b], _) when is_number(b), do: Kamath.max_complex(a, Complex.new(b)) 273 | def max_([a, %Complex{} = b], _) when is_number(a), do: Kamath.max_complex(Complex.new(a), b) 274 | 275 | def hamming_weight([x]) when is_integer(x) and x >= 0, do: Kamath.hamming_weight(x) 276 | 277 | def re([x], _) when is_number(x), do: x 278 | def re([%Complex{re: re}], _), do: re 279 | 280 | def im([x], _) when is_number(x), do: 0 281 | def im([%Complex{im: im}], _), do: im 282 | 283 | def phasor([x], _) when is_number(x), do: 0 284 | def phasor([%Complex{re: re, im: im}], _), do: Math.atan2(im, re) 285 | 286 | def as_complex([x], _) when is_number(x), do: Complex.new(x) 287 | def as_complex([%Complex{} = x], _), do: x 288 | 289 | def as_real([x], _) when is_number(x), do: x 290 | def as_real([%Complex{re: re}], _), do: re 291 | end 292 | -------------------------------------------------------------------------------- /lib/kamex/interpreter/special_forms.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Interpreter.SpecialForms do 2 | @moduledoc false 3 | 4 | import Kamex.Interpreter, only: [compute_expr: 2, compute_expr: 3] 5 | alias Kamex.Exceptions 6 | 7 | @tru 1 8 | @fals 0 9 | @falsey [[], @fals] 10 | 11 | @mapping [ 12 | quote: :quote, 13 | lambda: :lambda, 14 | def: :def_, 15 | defun: :defun, 16 | let: :let, 17 | "let-seq": :let_seq, 18 | if: :if_, 19 | or: :or_, 20 | and: :and_, 21 | atop: :atop, 22 | fork: :fork, 23 | bind: :bind 24 | ] 25 | 26 | def special_form?(name), do: Keyword.get(@mapping, name) 27 | 28 | def run(name, args, locals) do 29 | real_fn = Keyword.get(@mapping, name) 30 | 31 | apply(__MODULE__, real_fn, [args, locals]) 32 | end 33 | 34 | def quote([arg], locals), do: {arg, locals} 35 | 36 | def def_([name, value], locals) do 37 | value = compute_expr(value, locals) 38 | {value, Map.put(locals, name, value)} 39 | end 40 | 41 | # (let ((x (+ 2 2)) (y (- 5 2))) (+ x y)) 42 | def let([vars, expr], locals) do 43 | # vars are (name value) pairs 44 | vars = 45 | Enum.map(vars, fn [name, value] -> 46 | {name, compute_expr(value, locals)} 47 | end) 48 | |> Enum.into(locals) 49 | 50 | {compute_expr(expr, vars), locals} 51 | end 52 | 53 | def let_seq(args, og_locals) do 54 | # Sequenced, scoped operations. 55 | # (let-seq 56 | # (def x 3) 57 | # (defun add3 (y) (+ x y)) 58 | # (add3 2)) 59 | # Evaluates all `def` and `defun` calls and adds to locals, and computes and returns the first non-definer. 60 | # If only consists of `def` and `defun`, returns null 61 | 62 | {return, _} = 63 | Enum.reduce_while(args, {nil, og_locals}, fn node, {_, locals} -> 64 | case node do 65 | [name | _] = expr when name in [:def, :defun] -> 66 | {_, new_locals} = compute_expr(expr, locals, true) 67 | {:cont, {nil, new_locals}} 68 | 69 | expr -> 70 | {:halt, {compute_expr(expr, locals), locals}} 71 | end 72 | end) 73 | 74 | {return, og_locals} 75 | end 76 | 77 | def defun([name, input_args, body], locals) do 78 | {fun, _} = lambda([input_args, body], locals) 79 | {fun, Map.put(locals, name, fun)} 80 | end 81 | 82 | def lambda([input_args, body], locals) do 83 | # input_len = length(input_args) 84 | 85 | # `?arg` denotes optional arguments, which default to null/empty list 86 | # `...arg` denotes a rest arg (for variadics). default to [] if not given at runtime 87 | 88 | # Make sure that optionals only exist after all requireds, and that max one variadic exists, after all requireds and optionals 89 | # TODO: figure out adding this inside the parser 90 | # (lambda (x ?y ?z ...rest) body) OK 91 | # (lambda (x ?y ?z) body) OK 92 | # (lambda (x ...rest ?y ?z) body) BAD 93 | # (lambda (?x y) body) BAD 94 | # (lambda (...rest1 ...rest2) body) BAD 95 | input_args 96 | # Fancy stuff to turn a normal boring window into smth like `[[:x, nil], [:y, :x], [:"...z", :y]]` 97 | |> Enum.reverse() 98 | |> Enum.chunk_every(2, 1, [nil]) 99 | |> Enum.reverse() 100 | |> Enum.each(fn [curr, prev_] -> 101 | curr = to_string(curr) 102 | prev = to_string(prev_) 103 | 104 | # TODO: this could probably be a bit better 105 | cond do 106 | String.starts_with?(prev, "...") && !String.starts_with?(curr, "...") -> 107 | raise Exceptions.SyntaxError, "lambda: variadic arguments must come last" 108 | 109 | String.starts_with?(curr, "...") && String.starts_with?(prev, "...") -> 110 | raise Exceptions.SyntaxError, "lambda: can only contain one variadic argument" 111 | 112 | prev_ != nil && String.starts_with?(prev, "?") && 113 | !(String.starts_with?(curr, "?") || String.starts_with?(curr, "...")) -> 114 | raise Exceptions.SyntaxError, 115 | "lambda: optional arguments must come after required arguments" 116 | 117 | String.starts_with?("?...", curr) -> 118 | raise Exceptions.SyntaxError, 119 | "lambda: cannot combine optional and variadic arguments" 120 | 121 | true -> 122 | nil 123 | end 124 | end) 125 | 126 | has_rest = Enum.any?(input_args, fn x -> x |> to_string() |> String.starts_with?("...") end) 127 | optional_count = Enum.count(input_args, &String.starts_with?(to_string(&1), "?")) 128 | total_count = length(input_args) - if has_rest, do: 1, else: 0 129 | required_count = total_count - optional_count 130 | 131 | cleaned_args = 132 | Enum.map(input_args, fn x -> 133 | case to_string(x) do 134 | "..." <> arg -> String.to_atom(arg) 135 | "?" <> arg -> String.to_atom(arg) 136 | _ -> x 137 | end 138 | end) 139 | 140 | fun = fn called_args, called_local -> 141 | called_len = length(called_args) 142 | 143 | if called_len > total_count and not has_rest, 144 | # TODO: total_count will be more than required when optionals, need to add a detection to show a range 145 | do: 146 | raise( 147 | Exceptions.ArityError, 148 | "lambda: too many arguments, expected #{total_count} but got #{called_len}" 149 | ) 150 | 151 | optionals_fill = 152 | cond do 153 | called_len >= total_count -> 154 | [] 155 | 156 | called_len < required_count -> 157 | raise( 158 | Exceptions.ArityError, 159 | "lambda: too few arguments, expected #{total_count} but got #{called_len}" 160 | ) 161 | 162 | true -> 163 | # TODO: move this and anything using an empty list as null, to a proper `null`/`nil` atom, and make sure its handled appropriately. 164 | Stream.cycle([[]]) |> Enum.take(optional_count - (called_len - required_count)) 165 | end 166 | 167 | called_args = called_args ++ optionals_fill 168 | 169 | lamb_locals = 170 | if has_rest do 171 | {args, rest} = Enum.split(called_args, total_count) 172 | Enum.zip(cleaned_args, args ++ [rest]) 173 | else 174 | Enum.zip(cleaned_args, called_args) 175 | end 176 | |> Enum.into(called_local) 177 | 178 | compute_expr(body, lamb_locals) 179 | end 180 | 181 | {fun, locals} 182 | end 183 | 184 | def if_([condition, block | opt], locals) do 185 | else_block = List.first(opt) 186 | 187 | result = 188 | if compute_expr(condition, locals) in @falsey, 189 | do: compute_expr(else_block, locals), 190 | else: compute_expr(block, locals) 191 | 192 | {result, locals} 193 | end 194 | 195 | # TODO: first-value/first-nil? 196 | 197 | def or_(args, locals) do 198 | args 199 | |> Stream.map(fn node -> compute_expr(node, locals, true) end) 200 | |> Enum.find(fn 201 | {x, _} when x in @falsey -> false 202 | _ -> true 203 | end) || {@fals, locals} 204 | end 205 | 206 | def and_(args, locals) do 207 | args 208 | |> Stream.map(fn node -> compute_expr(node, locals, true) end) 209 | |> Enum.find(fn 210 | {x, _} when x in @falsey -> true 211 | _ -> false 212 | end) || {@tru, locals} 213 | end 214 | 215 | def atop(funs, locals) do 216 | nodes = atop_compose(funs) 217 | 218 | lambda([[:"...$1"], nodes], locals) 219 | end 220 | 221 | defp atop_compose([final]), do: [final, :"$1"] 222 | defp atop_compose([head | tail]), do: [head, atop_compose(tail)] 223 | 224 | def fork([to_call | args], locals) do 225 | nodes = [to_call | Enum.map(args, &atop_compose([&1]))] 226 | 227 | # TODO: does this work with multiple args? 228 | lambda([[:"$1"], nodes], locals) 229 | end 230 | 231 | def bind([fun | args], locals) do 232 | # Replace all occurances of :_ with a incrementally numbered argument :"$1", 233 | # :"$2", etc. Must not intrude into other `bind` calls, and if none 234 | # detected, just append new args to root function 235 | # 236 | # This functions a bit differently from og KamilaLisp, wheras they only 237 | # occurances of `:_` on the top level, we allow nested ones in other functions. 238 | {args, count} = bind_compose(args) 239 | lambda_args = Enum.map(1..(count - 1), fn i -> :"$#{i}" end) 240 | 241 | lambda([lambda_args, [fun | args]], locals) 242 | end 243 | 244 | defp bind_compose(item, count \\ 1) 245 | defp bind_compose(:_, count), do: {:"$#{count}", count + 1} 246 | defp bind_compose(x, count) when not is_list(x), do: {x, count} 247 | 248 | defp bind_compose([key | _] = item, count) when key in [:bind, :quote, :let, :lambda], 249 | do: {item, count} 250 | 251 | defp bind_compose(items, count) do 252 | {items, count} = 253 | Enum.reduce(items, {[], count}, fn item, {all, count} -> 254 | {item, count} = bind_compose(item, count) 255 | {all ++ [item], count} 256 | end) 257 | 258 | if count == 1, 259 | # Implicitly add placeholder to the end, if there aren't any placeholders. 260 | do: {items ++ [:"$1"], count + 1}, 261 | else: {items, count} 262 | end 263 | 264 | # TODO: cond 265 | end 266 | -------------------------------------------------------------------------------- /lib/kamex/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Parser do 2 | @moduledoc false 3 | alias Kamex.Exceptions 4 | 5 | defp cursed_list_helper(tail, do_parse \\ true) do 6 | # Last item exists to determine if we halted naturally reduce_while ended as 7 | # a result. Not sure if that's needed or could get caught by this first 8 | # clause here, will need to do some testing 9 | result = 10 | Enum.reduce_while(tail, {[], tail, true}, fn 11 | _, {_, [], _} -> 12 | raise Exceptions.ParserError, message: "unexpected end of input" 13 | 14 | _, {list, [{:"(", _} = a | tail], _} -> 15 | {new_list, tail} = cursed_list_helper(tail, false) 16 | 17 | # TODO: find last item and use that line number for closing (actually its probably the first one, do research dummy) 18 | {:cont, {[{:")", 999} | new_list] ++ [a | list], tail, true}} 19 | 20 | _, {list, [{:")", _} | tail], _} -> 21 | {:halt, {list, tail}} 22 | 23 | _, {list, [head | tail], _} -> 24 | {:cont, {[head | list], tail, true}} 25 | end) 26 | 27 | case result do 28 | {list, tail} -> 29 | list = if(do_parse, do: list |> Enum.reverse() |> parse(), else: list) 30 | {list, tail} 31 | 32 | _ -> 33 | raise(Exceptions.ParserError, message: "unexpected end of input") 34 | end 35 | end 36 | 37 | defp cursed_atop_helper(init, tail) do 38 | # Start chunk reverse so we can prepend to it easily 39 | {chunk, _, tail} = 40 | Enum.reduce_while(tail, {[init, :atop], :atop, tail}, fn 41 | # if last was atop and this atop, err, last atop and ident or bind, cont, otherwise end if not atop 42 | _, {_chunk, :atop, [{:atop, _} | _]} -> 43 | raise Exceptions.ParserError, 44 | message: 45 | "unexpected duplicate `@`. Multiple `@` must be separated by an identifier or bind" 46 | 47 | _, {chunk, :atop, [{:ident, _, _} = token | tail]} -> 48 | ident = parse(token) 49 | {:cont, {[ident | chunk], :ident, tail}} 50 | 51 | _, {chunk, :atop, [{:bind, _}, {:"(", _} | tail]} -> 52 | {list, tail} = cursed_list_helper(tail) 53 | {:cont, {[[:bind | list] | chunk], :bind, tail}} 54 | 55 | _, {_chunk, :atop, [{:bind, _} | _]} -> 56 | raise Exceptions.ParserError, 57 | message: "expected ( after $" 58 | 59 | _, {_chunk, :atop, [_ | _]} -> 60 | raise Exceptions.ParserError, 61 | message: "expected identifier or bind after `@` (atop)" 62 | 63 | # just push atop to prev 64 | _, {chunk, _prev, [{:atop, _} | tail]} -> 65 | {:cont, {chunk, :atop, tail}} 66 | 67 | _, {_chunk, prev, _tail} = acc when prev == :ident or prev == :bind -> 68 | {:halt, acc} 69 | end) 70 | 71 | chunk = Enum.reverse(chunk) 72 | {chunk, tail} 73 | end 74 | 75 | def parse(tokens) 76 | # def parse([]), do: raise(Exceptions.ParserError, message: "unexpected end of input") 77 | def parse([]), do: [] 78 | def parse({nil, _}), do: [] 79 | 80 | def parse({:tack, _, x}), do: [:tack, x] 81 | def parse({:atop, _}), do: raise(Exceptions.ParserError, message: "unexpected `@` (atop)") 82 | def parse({:int, _, int}), do: int 83 | def parse({:float, _, float}), do: float 84 | def parse({:complex, _, {real, im}}), do: Complex.new(real, im) 85 | def parse({:string, _, string}), do: string 86 | def parse({:ident, _, atom}), do: atom 87 | 88 | def parse([{:quot, _}, {:"(", _} | tail]) do 89 | # TODO: add a `Quoted` class to differentiate between syntax lists? 90 | # TODO: need to not parse inside here? 91 | {list, tail} = cursed_list_helper(tail) 92 | [[:quote, list] | parse(tail)] 93 | end 94 | 95 | def parse([{:quot, _}, {:ident, _, atom} | tail]), 96 | do: [[:quote, atom] | parse(tail)] 97 | 98 | # TODO: is there actually restrictions on what can be quoted 99 | def parse([{:quot, _} | _]), 100 | do: raise(Exceptions.ParserError, message: "expected identifier or list after quote") 101 | 102 | def parse([{:comment, _} | tail]), do: parse(tail) 103 | 104 | def parse([{:fork, _}, {:"(", _} | tail]) do 105 | {list, tail} = cursed_list_helper(tail) 106 | [[:fork | list] | parse(tail)] 107 | end 108 | 109 | def parse([{:bind, _}, {:"(", _} | tail]) do 110 | {list, tail} = cursed_list_helper(tail) 111 | 112 | case tail do 113 | [{:atop, _} | tail] -> 114 | {atop_chunk, tail} = cursed_atop_helper([:bind | list], tail) 115 | [atop_chunk | parse(tail)] 116 | 117 | _ -> 118 | [[:bind | list] | parse(tail)] 119 | end 120 | end 121 | 122 | def parse([{:fork, _} | _]), 123 | do: raise(Exceptions.ParserError, message: "expected ( after #") 124 | 125 | def parse([{:bind, _} | _]), 126 | do: raise(Exceptions.ParserError, message: "expected ( after $") 127 | 128 | def parse([{:map, _} | list]), do: [:bind, :map | [parse(list)]] 129 | 130 | def parse([{:partition, line} | tail]) do 131 | # TODO: this currently doesnt work if its at the root level of the syntax. What do we do? 132 | {list, tail} = cursed_list_helper(tail) 133 | # Re-add the parenthesis we consumed, to emulate adding one that didn't exist. 134 | [list | parse([{:")", line} | tail])] 135 | end 136 | 137 | def parse([{:")", _} | _]), 138 | do: raise(Exceptions.ParserError, message: "unexpected closing parenthesis") 139 | 140 | def parse([{:"(", _} | tail]) do 141 | {list, tail} = cursed_list_helper(tail) 142 | [list | parse(tail)] 143 | end 144 | 145 | def parse([{:ident, _, atom}, {:atop, _} | tail]) do 146 | {atop_chunk, tail} = cursed_atop_helper(atom, tail) 147 | [atop_chunk | parse(tail)] 148 | end 149 | 150 | def parse([head | tail]), do: [parse(head) | parse(tail)] 151 | end 152 | -------------------------------------------------------------------------------- /lib/kamex/util.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Util do 2 | @moduledoc false 3 | 4 | def sublist?([], _), do: false 5 | 6 | def sublist?([_ | t] = haystack, needle), 7 | do: List.starts_with?(haystack, needle) or sublist?(t, needle) 8 | end 9 | 10 | defimpl String.Chars, for: Complex do 11 | def to_string(%{re: re, im: im}), do: "#{re}J#{im}" 12 | end 13 | -------------------------------------------------------------------------------- /lib/kamex/util/comb.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Util.Comb do 2 | @moduledoc """ 3 | Utilities for combinatorics. 4 | """ 5 | 6 | def cartesian_product(lists) when is_list(lists) do 7 | lists 8 | |> Enum.reduce(&cart_prod([&1, &2])) 9 | |> Enum.map(fn x -> x |> List.flatten() |> Enum.reverse() end) 10 | end 11 | 12 | defp cart_prod([]), do: [] 13 | 14 | defp cart_prod([h]) do 15 | for x <- h, do: [x] 16 | end 17 | 18 | defp cart_prod([h | t]) do 19 | for a <- h, b <- cart_prod(t) do 20 | [a | b] 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/kamex/util/math.ex: -------------------------------------------------------------------------------- 1 | defmodule Kamex.Util.Math do 2 | @moduledoc false 3 | 4 | use Bitwise 5 | alias Kamex.Exceptions 6 | 7 | @inverse_e -0.36787944118 8 | @e1 Math.exp(1) 9 | @hamming_shift 0xFFFFFFFFFFFFFFFF 10 | 11 | # Compute the nth bernoulli number 12 | def bernoulli(n) when is_integer(n) do 13 | # TODO: cache and optimise 14 | if n > 2 && rem(n, 2) == 1 do 15 | # Shortcut as only B_1 and N_2n are of interest 16 | Ratio.new(0) 17 | else 18 | # TODO: lists instead of tuples 19 | Stream.transform(0..n, {}, fn m, acc -> 20 | acc = Tuple.append(acc, Ratio.new(1, m + 1)) 21 | 22 | # credo:disable-for-next-line 23 | cond do 24 | # TODO: figure out a shortcut when computing 25 | # m > 2 && rem(m, 2) == 1 -> 26 | # acc = Tuple.insert_at(acc, 0, Ratio.new(0)) 27 | # {[Ratio.new(0)], acc} 28 | 29 | m > 0 -> 30 | new = 31 | Enum.reduce(m..1, acc, fn j, acc -> 32 | put_elem( 33 | acc, 34 | j - 1, 35 | Ratio.mult(Ratio.new(j), Ratio.sub(elem(acc, j - 1), elem(acc, j))) 36 | ) 37 | end) 38 | 39 | {[elem(new, 0)], new} 40 | 41 | true -> 42 | {[elem(acc, 0)], acc} 43 | end 44 | end) 45 | |> Enum.to_list() 46 | |> List.last() 47 | end 48 | end 49 | 50 | # --- 51 | 52 | # TODO: flip arg names around to match wiki? 53 | def jacobi(_, k) when k <= 0 or (k &&& 1) == 0, 54 | do: 55 | raise(Exceptions.MathError, 56 | message: "cannot compute jacobi for k <= 0 or k is even" 57 | ) 58 | 59 | def jacobi(n, k) when n < 0 do 60 | j = ja(-n, k) 61 | 62 | case k &&& 3 do 63 | 1 -> j 64 | 3 -> -j 65 | end 66 | end 67 | 68 | def jacobi(n, k) when is_integer(n) and is_integer(k), do: ja(rem(n, k), k) 69 | 70 | defp ja(0, _), do: 0 71 | defp ja(1, _), do: 1 72 | defp ja(n, k) when n >= k, do: ja(rem(n, k), k) 73 | 74 | defp ja(n, k) when (n &&& k) == 0 do 75 | j = ja(n >>> 1, k) 76 | 77 | case k &&& 7 do 78 | 1 -> j 79 | 3 -> -j 80 | 5 -> -j 81 | 7 -> j 82 | end 83 | end 84 | 85 | defp ja(n, k) do 86 | j = ja(k, n) 87 | 88 | if (n &&& 3) == 3 and (k &&& 3) == 3, 89 | do: -j, 90 | else: j 91 | end 92 | 93 | # --- 94 | 95 | def lambert_w(x) when x < @inverse_e, 96 | do: raise(Exceptions.MathError, message: "cannot compute lambert w for x < -1/e") 97 | 98 | # Can't log 0, so shortcut to return 0 99 | def lambert_w(0), do: 0 100 | 101 | # TODO: replace floats everywhere with decimals 102 | def lambert_w(x) do 103 | w = 104 | cond do 105 | x < 0.06 && x * 2 * @e1 + 2 <= 0 -> 106 | -1 107 | 108 | x < 0.06 -> 109 | ti = x * 2 * @e1 + 2 110 | t = Ratio.new(Math.sqrt(ti)) 111 | tsq = Ratio.mult(t, t) 112 | 113 | Ratio.new(-1) 114 | |> Ratio.add(Ratio.mult(t, Ratio.new(1, 6))) 115 | |> Ratio.add(Ratio.mult(tsq, Ratio.new(257, 720))) 116 | |> Ratio.add(tsq |> Ratio.mult(t) |> Ratio.mult(Ratio.new(13, 720))) 117 | |> Ratio.div( 118 | Ratio.new(1) 119 | |> Ratio.add(Ratio.mult(t, Ratio.new(5, 6))) 120 | |> Ratio.add(Ratio.mult(tsq, Ratio.new(103, 720))) 121 | ) 122 | |> Ratio.to_float() 123 | 124 | x < 1.363 -> 125 | l1 = Math.log(x + 1) 126 | l1 * ((1 - Math.log(l1 + 1)) / (l1 + 2)) 127 | 128 | x < 3.7 -> 129 | l1 = Math.log(x) 130 | l2 = Math.log(l1) 131 | 132 | l1 - l2 - Math.log((l2 / l1 - 1) / 2) 133 | 134 | true -> 135 | l1 = Math.log(x) 136 | l2 = Math.log(l1) 137 | 138 | d1 = 2 * l1 * l1 139 | d2 = 3 * l1 * d1 140 | d3 = 2 * l1 * d2 141 | d4 = 5 * l1 * d3 142 | 143 | (l1 - l2) 144 | |> then(&(&1 + l2 / l1)) 145 | |> then(&(&1 + l2 * (l2 - 2) / d1)) 146 | |> then(&(&1 + l2 * (6 + l2 * (-9 + 2 * l2)) / d2)) 147 | |> then(&(&1 + l2 * (-12 + l2 * (36 + l2 * (-22 + 3 * l2))) / d3)) 148 | |> then(&(&1 + l2 * (60 + l2 * (-360 + l2 * (350 + l2 * (-125 + 12 * l2)))) / d4)) 149 | end 150 | 151 | if w == -1 do 152 | w 153 | else 154 | tol = 1.0e-16 155 | 156 | Enum.reduce_while(1..200, w, fn _, v -> 157 | if v == 0 do 158 | {:halt, v} 159 | else 160 | w1 = v + 1 161 | zn = Math.log(x / v) - v 162 | qn = w1 * 2 * (w1 + 2 * (zn / 3)) 163 | en = zn / w1 * (qn - zn) / (qn - zn * 2) 164 | wen = v * en 165 | v = v + wen 166 | 167 | if abs(wen) < tol, do: {:halt, v}, else: {:cont, v} 168 | end 169 | end) 170 | end 171 | end 172 | 173 | # --- 174 | 175 | def gaussian_rem(%Complex{} = a, %Complex{} = b) do 176 | prod = Complex.mult(a, Complex.conjugate(b)) 177 | p = prod.re / (b.re ** 2 + b.im ** 2) 178 | q = prod.im / (b.re ** 2 + b.im ** 2) 179 | gamma = Complex.new(p, trunc(q)) 180 | 181 | Complex.sub(a, Complex.mult(gamma, b)) 182 | end 183 | 184 | def norm_complex(%Complex{} = x) do 185 | y = Math.sqrt(x.re ** 2 + x.im ** 2) 186 | Complex.div(x, Complex.new(y)) 187 | end 188 | 189 | # TODO: make sure that this comparison actually works properly 190 | def min_complex(%Complex{} = a, %Complex{} = b) do 191 | if a < b, do: a, else: b 192 | end 193 | 194 | def max_complex(%Complex{} = a, %Complex{} = b) do 195 | if a > b, do: a, else: b 196 | end 197 | 198 | def gcd_complex(%Complex{} = a, %Complex{} = b) do 199 | if gaussian_rem(a, b) == Complex.new(0), 200 | do: b, 201 | else: gcd_complex(b, gaussian_rem(a, b)) 202 | end 203 | 204 | def lcm_complex(%Complex{} = a, %Complex{} = b), 205 | do: Complex.div(Complex.mult(a, b), gcd_complex(a, b)) 206 | 207 | # --- 208 | 209 | def popcount(n) when is_integer(n), do: popcount(<>, 0) 210 | defp popcount(<<>>, acc), do: acc 211 | defp popcount(<>, acc), do: popcount(rest, acc + bit) 212 | 213 | # TODO: not so sure about this implementation, need to take another look at it 214 | def hamming_weight(n) when is_integer(n), do: hamming_weight(n, 0) 215 | defp hamming_weight(x, total) when x <= 0, do: total 216 | 217 | defp hamming_weight(x, total), 218 | # is this totally necessary? 219 | do: hamming_weight(x >>> 64, total + popcount(x &&& @hamming_shift)) 220 | end 221 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Kamex.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :kamex, 7 | version: "0.1.0", 8 | elixir: "~> 1.13", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | xref: [exclude: [Extractable]] 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | {:credo, "~> 1.6", only: :dev}, 26 | {:complex, "~> 0.3.0"}, 27 | {:math, "~> 0.7.0"}, 28 | {:ratio, "~> 3.0"}, 29 | {:tensor, "~> 2.1"}, 30 | {:typed_struct, "~> 0.2.1"} 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, 4 | "complex": {:hex, :complex, "0.3.0", "cf1785c827123bc7a9da9e275fd0d3c955b2b823d10c513725360e5c1f03129a", [:mix], [{:exprintf, "~> 0.1", [hex: :exprintf, repo: "hexpm", optional: false]}], "hexpm", "41ab27c6dcc07e5640ae06dd8b38d81e1abfe6f9dfa69f734cafaba83d384c40"}, 5 | "credo": {:hex, :credo, "1.6.1", "7dc76dcdb764a4316c1596804c48eada9fff44bd4b733a91ccbf0c0f368be61e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "698607fb5993720c7e93d2d8e76f2175bba024de964e160e2f7151ef3ab82ac5"}, 6 | "exprintf": {:hex, :exprintf, "0.2.1", "b7e895dfb00520cfb7fc1671303b63b37dc3897c59be7cbf1ae62f766a8a0314", [:mix], [], "hexpm", "20a0e8c880be90e56a77fcc82533c5d60c643915c7ce0cc8aa1e06ed6001da28"}, 7 | "extractable": {:hex, :extractable, "0.2.1", "cf32f0cf2328c073505be285fedecbc984a4f5fec300370dc9c6125bf4d99975", [:mix], [], "hexpm", "42532209510e365c3c9a56c33a2b5448ab1645c0ae124f261162f2c166061019"}, 8 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 9 | "insertable": {:hex, :insertable, "0.2.0", "f9ef5e484d1cc0756f1d248a54466aa6388142a81df18f588158e3eda2501395", [:mix], [], "hexpm", "518a8b5870344c784dec4560a296e3ef539cd08e58df7f425485bb6158ab70d6"}, 10 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 11 | "math": {:hex, :math, "0.7.0", "12af548c3892abf939a2e242216c3e7cbfb65b9b2fe0d872d05c6fb609f8127b", [:mix], [], "hexpm", "7987af97a0c6b58ad9db43eb5252a49fc1dfe1f6d98f17da9282e297f594ebc2"}, 12 | "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, 13 | "ratio": {:hex, :ratio, "3.0.0", "e2de6bd5f6cd393496d93eb29a409ef7fb9c70defb691bd471b8f7379941c9eb", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "21b1c9d203b4a05790a8c2b36237f705f83135850bfaab207d3e82192e0de028"}, 14 | "tensor": {:hex, :tensor, "2.1.2", "2f0569f922300e5fa09128481b9da418190e9f11c701d3777c4d0f60eac12806", [:mix], [{:extractable, "~> 0.2.0", [hex: :extractable, repo: "hexpm", optional: false]}, {:fun_land, "~> 0.9.0", [hex: :fun_land, repo: "hexpm", optional: true]}, {:insertable, "~> 0.2.0", [hex: :insertable, repo: "hexpm", optional: false]}, {:numbers, "~> 5.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "edc770c9e32fcf9c50e14808541dcba226370b0fec7276c4f551b5ea910dbdbc"}, 15 | "typed_struct": {:hex, :typed_struct, "0.2.1", "e1993414c371f09ff25231393b6430bd89d780e2a499ae3b2d2b00852f593d97", [:mix], [], "hexpm", "8f5218c35ec38262f627b2c522542f1eae41f625f92649c0af701a6fab2e11b3"}, 16 | } 17 | -------------------------------------------------------------------------------- /src/lexer.xrl: -------------------------------------------------------------------------------- 1 | Definitions. 2 | 3 | IdentStartChar = [^'"\\\s\r\n\t\f\(\)\[\],@#$:\\;] 4 | % ' 5 | IdentChar = [^'"\s\r\n\t\f\(\)\[\],@;] 6 | % ' 7 | 8 | Digit = [0-9] 9 | Int = (-?{Digit}+) 10 | Exponent = ([eE]{Int}) 11 | Decimal = \.{Digit}+ 12 | Float = ({Int}({Exponent}|({Decimal}{Exponent}?))) 13 | ComplexPart = (({Float})|({Int})) 14 | Hex = [0-9a-fA-F] 15 | 16 | FullIdent = {IdentStartChar}{IdentChar}* 17 | Whitespace = [\s\t\n\r,] 18 | 19 | Rules. 20 | 21 | % Lists & nil 22 | nil : {token, {nil, TokenLine}}. 23 | \( : {token, {'(', TokenLine}}. 24 | \) : {token, {')', TokenLine}}. 25 | 26 | % Special modifiers for idents/functions 27 | \$ : {token, {bind, TokenLine}}. 28 | #{Digit}+ : {token, {tack, TokenLine, tack_to_int(TokenChars)}}. 29 | # : {token, {fork, TokenLine}}. 30 | : : {token, {map, TokenLine}}. 31 | @ : {token, {atop, TokenLine}}. 32 | \\ : {token, {partition, TokenLine}}. 33 | ' : {token, {quot, TokenLine}}. % ' 34 | 35 | % Literals 36 | 0[xX]{Hex}+ : {token, {int, TokenLine, hex_to_int(TokenChars)}}. 37 | 0[bB][10]+ : {token, {int, TokenLine, binary_to_int(TokenChars)}}. 38 | "(\\.|\r?\n|[^\\\n\"])*" : {token, {string, TokenLine, list_to_binary(clean_str(TokenChars))}}. 39 | {ComplexPart}J{ComplexPart} : {token, {complex, TokenLine, list_to_complex(TokenChars)}}. 40 | {Float} : {token, {float, TokenLine, list_to_float(TokenChars)}}. 41 | {Int} : {token, {int, TokenLine, list_to_integer(TokenChars)}}. 42 | 43 | {FullIdent}+ : {token, {ident, TokenLine, list_to_atom(TokenChars)}}. 44 | 45 | % Garbage 46 | ;.* : {token, {comment, TokenLine}}. 47 | {Whitespace}+ : skip_token. 48 | 49 | Erlang code. 50 | 51 | clean_str(Str) when is_list(Str) -> string:trim(Str, both, "\""). 52 | 53 | tack_to_int([$# | Num]) -> list_to_integer(Num). 54 | hex_to_int([$0, _ | Num]) -> list_to_integer(Num, 16). 55 | binary_to_int([$0, _ | Num]) -> list_to_integer(Num, 2). 56 | 57 | list_to_complex(Str) when is_list(Str) -> 58 | [Real, Im] = string:split(Str, "J"), 59 | {list_to_num(Real), list_to_num(Im)}. 60 | 61 | % atop_to_idents(Atop) when is_list(Atop) -> 62 | % Tokens = string:tokens(Atop, "@"), 63 | % lists:map(fun(T) -> list_to_atom(T) end, Tokens). 64 | 65 | list_to_num(Str) when is_list(Str) -> 66 | % TODO: i dont think erlang supports `1e3` but does `1.2e3`. need to properly look 67 | IsFloat = lists:member($., Str) or lists:member($e, Str) or lists:member($E, Str), 68 | 69 | if 70 | IsFloat -> list_to_float(Str); 71 | true -> list_to_integer(Str) 72 | end. -------------------------------------------------------------------------------- /test/kamex.exs: -------------------------------------------------------------------------------- 1 | defmodule KamexTest do 2 | use ExUnit.Case 3 | doctest Kamex 4 | 5 | test "greets the world" do 6 | assert Kamex.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------