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

Add erlang-module support #2094

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

fnchooft
Copy link
Contributor

@fnchooft fnchooft commented Jul 19, 2023

The code is deemed a module definition if the first line start with "-module(".

In this case the entire code-block is interpreted as erlang-module and if there are no errors the module is compiled and loaded.

An example for a working livebook (livemd-file):

Untitled notebook

Erlang - on the fly ( inline_module ) via Livebook

-module(inline_module2).

-export([go/1]).

go(jose) -> {ok,<<"thanks Jose! :)">>};
go(fabian) -> {ok,<<"kkkk - this Elixir-code is heavily Erlang-flavoured heim??? :)">>};
go(_) -> {ok, <<"thanks contributers!">>}.
inline_module2:go(jose).

@CLAassistant
Copy link

CLAassistant commented Jul 19, 2023

CLA assistant check
All committers have signed the CLA.

Enum.map(
statements,
fn str ->
{:ok, result, _} = :erl_scan.string(String.to_charlist(str))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to handle errors accordingly. It will be similar to the eval_statements branch, there is probably a lot we can share between the two. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback - will adapt based on the original-code.

@@ -675,6 +675,47 @@ defmodule Livebook.Runtime.Evaluator do
end

defp eval(:erlang, code, binding, env) do
is_module = String.starts_with?(code, "-module(")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of detecting based on a string, we can do the first pass with :erl_scan.string(String.to_charlist(str)) and then traverse all entries and see if any defines a module attribute.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your opinion: Analyze the forms to only allow one module? Throw an error to instruct the user?
Seems more inline with how Erlang defines modules.

If you defined multiple modules we would have to have a lot more checks, and we would have to seperate the statements per module.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, for now, I think the best is to:

  1. Check if it has attributes
  2. If it has attributes, one of them (no more, no less) has to be a module attribute

@josevalim
Copy link
Contributor

This is very exciting @fnchooft, I have added some comments. We need error handling and tests. :)

@fnchooft
Copy link
Contributor Author

Hi Jose, still W.I.P...

Changes:

  • Basic try-catch for error-handling
  • Use the scanner for the whole code-block
  • Create forms from tokens - Erlang-code-conversion - I am not Elixir apt-yet.... :)
  • TODO: Refactor the common parts
  • TODO: Add tests

Enum.map(
form_statements,
fn {form_statement} ->
{:ok, form} = :erl_parse.parse_form(form_statement)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of try/catch below, we should pattern match and handle those forms similar to the other branch. You can look into Elixir's with (which is similar to Erlang's maybe).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been cleaned up in the manner you proposed.

Comment on lines 685 to 688
catch
kind, error ->
stacktrace = prune_stacktrace(:erl_eval, __STACKTRACE__)
{{:error, kind, error, stacktrace}, []}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this outer try-catch is not necessary. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Morning - this has been cleaned up in latest update.

@jonatanklosko
Copy link
Member

jonatanklosko commented Jul 20, 2023

Also note that we need to recognise which modules are defined and used by each evaluation and return this information, similarly to used_vars:

# Simple heuristic to detect the used variables. We look at
# the tokens and assume all var tokens are used variables.
# This will not handle shadowing of variables in fun definitions
# and will only work well enough for expressions, not for modules.
used_vars =
for {:var, _anno, name} <- tokens,
do: {erlang_to_elixir_var(name), nil},
into: MapSet.new(),
uniq: true

For Elixir we use tracer, but as a reference that's how the entries look like:

# Modules
identifiers_used =
for module <- tracer_info.modules_used,
do: {:module, module},
into: identifiers_used
identifiers_defined =
for {module, _vars} <- tracer_info.modules_defined,
version = module.__info__(:md5),
do: {{:module, module}, version},
into: identifiers_defined

@fnchooft
Copy link
Contributor Author

@jonatanklosko - Thanks for the insight! Question: In the case we would limit the Code-erlang-block to the definition of only one Erlang-module, not allowing other expressions for instance, would it be enough to only add the module_name to the used_identifiers?

@jonatanklosko
Copy link
Member

If we enforce just a single module we can compute md5 of the source as the module version or phash of the forms, because we would know all the code is about the module.

Copy link
Contributor

@josevalim josevalim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Can you please add some tests that exercise the error branches too? Some that make the tokenizer fail after a module is defined. And another that makes the parser file :) thanks!

The code is deemed a module definition if the first line
start with "-module(".

In this case the entire code-block is interpreted as
erlang-module and if there are no errors the module is compiled
and loaded.

Basic error handling has been added
Basic tests have been added
@fnchooft
Copy link
Contributor Author

Afternoon - had some time to add:

  • More tests ( expression after module / test for duplicate function definitions etc )

@jonatanklosko - I tried adding only the modulename + md5 to the logic - as you suggested.
However it still is not 100% ok.

For instance - it is not detecting correctly if a call to a function in the module is stale or not.
If the function is not defined the error-handling is ok, however it would be nice to get the "stale"-indication on the erlang-code-cell.

@jonatanklosko
Copy link
Member

jonatanklosko commented Jul 25, 2023

For instance - it is not detecting correctly if a call to a function in the module is stale or not.

Yeah, and the same is going to be the case if you define a module with go/0, have another cell call go()., and then change the module implementation. We need to analyse the AST to find function calls and from that get a list of modules used by the given evaluation (cell).

For elixir we use compiler tracer to get the referenced modules and we then we use this information here:

identifiers_used =
for module <- tracer_info.modules_used,
do: {:module, module},
into: identifiers_used

@josevalim I'm thinking that ideally we would emit the tracer events programmatically (defining a module, referencing functions) and then the rest is handled using the same code path. Does that make sense, or is there any reason not to do that?

@josevalim
Copy link
Contributor

@jonatanklosko emulating the tracer or the tracer_info is perfectly fine for me. The only concern is that there is no env.

@jonatanklosko
Copy link
Member

Ah true, actually it should be simpler to use %Evaluator.Tracer{} together with Evaluator.Tracer.apply_updates/2 directly. If there are issues, we can have something more separate.

Other notes: we need to persist the module bytecode with Evaluator.write_module!(module, bytecode) and also add it to env.context_modules.

@fnchooft
Copy link
Contributor Author

@jonatanklosko - Apologies for letting this simmer for some months.

I have not been following changes in livebook - but is there still value in this approach? If I have some time I will rebase and re-read the comments.

Kind regards,
Fabian

@jonatanklosko
Copy link
Member

@fnchooft no worries, nothing changed in this matter, so sounds good :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants