OvermindDL1
MLElixir - attempting an ML-traditional syntax entirely within the Elixir AST
Been making an MLElixir thing (not released yet…) for fun in spare time in the past day. I’m just trying to see how much I can get an ML-traditional syntax entirely within the Elixir AST, while being properly typed (with occasional fun with Refined Types and such). An example IEX session with it:
- Basic Types (adding more over time)
iex> import MLElixir
MLElixir
iex> defml 1
1
iex> defml 6.28
6.28
iex> defml :ok
:ok
- Let untyped variable bindings:
iex> defml let _a = 2 in 1
1
iex> defml let a = 1 in a
1
iex> defml let a = 42 in
...> let b = a in
...> let c = b in
...> c
42
- Let Typed variable bindings (The errors are very simplistic and not descriptive right now, still debugging time after all):
iex> defml let ![a: int] = 1 in a
1
iex> defml let ![a: float] = 6.28 in a
6.28
iex> defml let a = 1 in
...> let ![b: int] = a in
...> b
1
iex> defml let ![a: int] = 6.28 in a
** (MLElixir.UnificationError) Unification error between `{:"$$TCONST$$", :float, [values: [6.28]]}` and `{:"$$TCONST$$", :int, []}` with message: Unable to resolve mismatched types
(typed_elixir) lib/ml_elixir.ex:566: MLElixir.resolve_types!/3
(typed_elixir) lib/ml_elixir.ex:250: MLElixir.resolve_binding/3
(typed_elixir) lib/ml_elixir.ex:163: MLElixir.parse_let/3
(typed_elixir) expanding macro: MLElixir.defml/1
iex:16: (file)
- Let Refined Typed variable bindings:
iex> defml let ![a: int a=1] = 1 in a
1
iex> defml let ![a: int a<=2] = 1 in a
1
iex> defml let ![a: int a>=2] = 1 in a
** (MLElixir.UnificationError) Unification error between `{:"$$TCONST$$", :int, [values: [1]]}` and `{:"$$TCONST$$", :int, [values: [{2, :infinite}]]}` with message: Unable to resolve
(typed_elixir) lib/ml_elixir.ex:566: MLElixir.resolve_types!/3
(typed_elixir) lib/ml_elixir.ex:250: MLElixir.resolve_binding/3
(typed_elixir) lib/ml_elixir.ex:163: MLElixir.parse_let/3
(typed_elixir) expanding macro: MLElixir.defml/1
iex:6: (file)
Function calls (shown here via +):
iex> defml 1+2
3
iex> defml 1.1+2.2
3.3000000000000003
iex> defml 1+2.2
** (MLElixir.UnificationError) Unification error between `{:"$$TCONST$$", :int, [values: [1]]}` and `{:"$$TCONST$$", :float, [values: [2.2]]}` with message: Unable to unify types
(typed_elixir) lib/ml_elixir.ex:712: MLElixir.unify_types!/3
(typed_elixir) lib/ml_elixir.ex:108: anonymous fn/3 in MLElixir.Core.__ml_open__/0
(typed_elixir) lib/ml_elixir.ex:199: MLElixir.parse_ml_expr/2
(typed_elixir) lib/ml_elixir.ex:145: MLElixir.defml_impl/2
(typed_elixir) expanding macro: MLElixir.defml/1
iex:2: (file)
Opening (‘import’ in Elixir parlance) another module (also showing how to disable the Core opens, as you can see it is the Core that defines the + function):
iex> defml let open MLElixir.Core in 1+2
3
iex> defml no_default_opens: true, do: let open MLElixir.Core in 1+2
3
iex> defml no_default_opens: true, do: 1+2
** (MLElixir.InvalidCall) 6:Invalid call of `+` because of: No such function found
(typed_elixir) lib/ml_elixir.ex:196: MLElixir.parse_ml_expr/2
(typed_elixir) lib/ml_elixir.ex:145: MLElixir.defml_impl/2
(typed_elixir) expanding macro: MLElixir.defml/1
iex:6: (file)
Most Liked
derpydev
Sorry to be a bit off topic, but I feel like you missed an great opportunity in not calling this project ExML…ok I’ll show myself out now…
Seriously though awesome project!
josevalim
Reading this thread has been such an emotional rollercoaster. 
To sum up, -> is only allowed between do/end, fn/end and (/). But you need to be careful because the parens need to apply to the -> and not arguments. The reason why foo(fun x -> x) does not work becomes clearer if you add multiple arguments. If you have foo(fun x, y -> x), does it mean foo(fun x, (y -> x)) or foo((fun x, y -> x))?
The reason I wrote the document linked by Eric is exactly because we wanted to show it is less rules than most would expect. Especially because we don’t need to specify the rules for keywords like case, def, defmodule, receive, if, try, etc.
ericmj
The difference between quote do fun x -> x end and defml fun x -> x is exactly the block. quote is not magically cheating in any way, the parser accepts -> in quote because it’s inside a block. When you call your defml macro you don’t wrap -> in a block.
If you use fn x -> x instead of fun x -> x you need to also end the block with an end since fn creates a block just like do.
It’s easy to remember what creates blocks in Elixir because there are only three ways to do it. do ... end, fn ... end and ( ... ). do blocks are in fact just sugar for ( ... ) and they are represented the same way in the syntax tree.
Check the “Blocks” section in the docs [1] for more details.
[1] https://hexdocs.pm/elixir/master/syntax-reference.html#syntax-sugar
OvermindDL1
Oh I just caught that myself too! That is brilliant. ^.^
EDIT:
Vwoop:
defmacro defml(opts) when is_list(opts) do
case opts[:do] do
nil -> defml_impl(opts, [])
ast -> defml_impl(ast, opts)
end
end
I can probably live with requiring parenthesis around anonymousall function definitions, though would be nice if an infix operator like => (much as I stylistically dislike fat arrows) would work like normal infix operators (-> is really weird… not like a normal operator…). 
EDIT: What no strikethrough markdown operator on this forum? o.O
EDIT: For note, I’m trying to translate a mini-language I made into Elixir’s AST to use straight in Elixir (right now it parses out text into elixir ast, which makes syntax coloring really bad when used like defml "let a = 42 in a", although it works, just… ugly, we need read-macro’s in elixir ^.^), where its function syntax is:
let myAdder = fun
| (a:int) (b:int b>=0) -> a + b
| (a:int) (b:int) -> a - b
| (a:binary) (b:binary) -> String.concat [a, b]
| a b -> error "unsupported"
in myAdder 10 (-4)
Types are optional if it can be inferred, though fully typed it would look more like:
let myAdder = fun
| (a : int) (b : int b>=0) : int a+b -> a + b (* Since a has a valid range of infinite here, the return type is infinite+{b's range, which is {0, infinite}}, which is still infinite regardless, it is not that smart yet *)
| (a : int) (b : int) : int a-b -> a - b (* int's extra information holds possible values allowed, simple math allowed on those values *)
| (a : string) (b : string) : string -> String.concat [a, b] (* string's can refine on length only, currently *)
| (a : any) (b : any) -> error "unsupported" (* any's do not have any refinement yet *)
in let curried = myAdder "Hello " (* A new erlang anonymous function is made here that curries myAdder, curried things and closures cannot be made into top-level module functions in this play language (type error) *)
in curried "world" (* Now I call it *)
josevalim
Yes, yes! I didn’t mean it as a negative thing. More like: “will he make it?!”.
The reason we didn’t use tokenization macros is because they are quite more powerful than macros, allowing anyone to create their own syntax for Elixir. It would be wonders for what you are doing but could make Elixir a really hard language, as totally new syntax constructs could be introduced any time.







