diff --git a/.dockerignore b/.dockerignore index cbf7b8b..606fc62 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ **/_compiled/ /bin/ /Makefile -**/erl_crash.dump \ No newline at end of file +**/erl_crash.dump +bootstraped \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7dc53d8..b97976d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ src/_compiled /example/ **/erl_crash.dump -fy_doc/_env +libs/fy_doc/_env +bootstraped diff --git a/Makefile b/Makefile index 167d844..55724d9 100644 --- a/Makefile +++ b/Makefile @@ -4,28 +4,52 @@ SHELL := /bin/bash ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) # fython src folder SRC_DIR:="$(ROOT_DIR)/src" +TESTS_PATH:="$(ROOT_DIR)/tests" -bootstrap-with-docker: - @DOCKER_TAG="fython:bootstrap" - docker build -f devtools/Dockerfile -t $$DOCKER_TAG --target base . +BOOTSTRAP_DOCKER_TAG:="fython:bootstrap" +COMPILER_DOCKER_TAG:="fython:compiler" +SHELL_DOCKER_TAG:="fython:shell" +FYTEST_DOCKER_TAG:="fython:fytest" +TESTS_DOCKER_TAG:="fython:tests" .ONESHELL: +bootstrap-with-docker: + DOCKER_BUILDKIT=1 docker build -f devtools/Dockerfile -t $(BOOTSTRAP_DOCKER_TAG) --target bootstrap . \ + && docker rm -f fython_bootstrap || true \ + && docker run \ + --name fython_bootstrap \ + -v $(ROOT_DIR)/bootstraped:/final_bootstrap \ + -e FINAL_PATH='/final_bootstrap' \ + $(BOOTSTRAP_DOCKER_TAG) && echo "Bootstrap finished. Result saved at '$(ROOT_DIR)/bootstraped'" + compile-project: # USAGE: # > make compile-project path=/home/joao/fython/example - @DOCKER_TAG="fython:compiler" - docker build -f devtools/Dockerfile -t $$DOCKER_TAG --target compiler . - docker run --env PROJET_FOLDER=/project -v $(path):/project $$DOCKER_TAG + DOCKER_BUILDKIT=1 docker build -f devtools/Dockerfile -t $(COMPILER_DOCKER_TAG) --target compiler . \ + && DOCKER_BUILDKIT=1 docker run --env PROJET_FOLDER=/project$(path) -v $(path):/project$(path) $(COMPILER_DOCKER_TAG) -.ONESHELL: run-tests: - @DOCKER_TAG="fython:tests" - docker build -f devtools/Dockerfile -t $$DOCKER_TAG --target tests . - docker run $$DOCKER_TAG + # > make run-tests + $(MAKE) build-fytest \ + && DOCKER_BUILDKIT=1 docker run -e FOLDER=$(TESTS_PATH) -v $(TESTS_PATH):$(TESTS_PATH) $(FYTEST_DOCKER_TAG) +build-shell: + DOCKER_BUILDKIT=1 docker build -f devtools/Dockerfile -t $(SHELL_DOCKER_TAG) --target shell . + +build-fytest: + DOCKER_BUILDKIT=1 docker build -f devtools/Dockerfile -t $(FYTEST_DOCKER_TAG) --target fytest . -.ONESHELL: shell-current-src: - @DOCKER_TAG="fython:shell" - docker build -f devtools/Dockerfile -t $$DOCKER_TAG --target shell . - docker run -it $$DOCKER_TAG \ No newline at end of file + $(MAKE) build-shell \ + && DOCKER_BUILDKIT=1 docker run -it $(SHELL_DOCKER_TAG) + +project-shell: + # > project-shell path=/home/joao/fython/example + $(MAKE) compile-project path=$(path) \ + && $(MAKE) build-shell\ + && DOCKER_BUILDKIT=1 docker run -it --env ADITIONAL_PATHS=/project$(path)/_compiled -v $(path):/project$(path) $(SHELL_DOCKER_TAG) + +project-bash: + # > project-bash path=/home/joao/fython/example + $(MAKE) compile-project path=$(path) \ + && DOCKER_BUILDKIT=1 docker run -it --env ADITIONAL_PATHS=/project$(path)/_compiled -v $(path):/project$(path) $(SHELL_DOCKER_TAG) bash diff --git a/devtools/Dockerfile b/devtools/Dockerfile index b8f232b..115680c 100644 --- a/devtools/Dockerfile +++ b/devtools/Dockerfile @@ -17,35 +17,57 @@ RUN wget https://github.com/joaovitorsilvestre/fython/releases/download/$VERSION COPY src src COPY /devtools/bootstrap.sh /bootstrap.sh +COPY /devtools/bootstrap_old.sh /bootstrap_old.sh COPY /devtools/fython /fython RUN chmod a+x /bootstrap.sh +RUN chmod a+x /bootstrap_old.sh RUN chmod a+x /fython # Execute bootstrap RUN echo "Bootstraping using Fython $VERSION_TO_USE_AS_BOOTSTRAPER" \ - && ./bootstrap.sh compile /src $DESTINE_PATH $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER $ELIXIR_BEAMS_PATH + && ./bootstrap_old.sh compile /src $DESTINE_PATH $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER $ELIXIR_BEAMS_PATH # Remove base used for bootstrap (to ensure that we are not using it anymore) RUN rm -rf $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER +FROM base as bootstrap +ENV FIRST_PATH="/test_compiled1" +ENV SECOND_PATH="/test_compiled2" + # Check if it can recompile itself (its a good way to test while we dont have proper testing) -#RUN ./bootstrap.sh compile /src /test_compiled1 $DESTINE_PATH $ELIXIR_BEAMS_PATH -#RUN ./bootstrap.sh compile /src /test_compiled2 /test_compiled1 +RUN ./bootstrap.sh compile /src $FIRST_PATH $DESTINE_PATH $ELIXIR_BEAMS_PATH +RUN ./bootstrap.sh compile /src $SECOND_PATH $FIRST_PATH $ELIXIR_BEAMS_PATH -FROM base as elixir_shell -# TODO -# CMD erl -pa $IEX_BEAMS_PATH -noshell -user Elixir.IEx.CLI +iex +# The final version bootstraped ready for release +ENV FINAL_PATH="/bootstraped" +CMD ./bootstrap.sh compile /src $FINAL_PATH $SECOND_PATH $ELIXIR_BEAMS_PATH FROM base as shell -CMD /fython exec "Shell.start()" $DESTINE_PATH +ENV ADITIONAL_PATHS="" +ENV SHELL_PATH="/libs/shell" +ENV SHELL_PATH_COMPILED="/libs/shell/_compiled" + +COPY libs/shell $SHELL_PATH +RUN /fython exec "Core.Code.compile_project('$SHELL_PATH')" $DESTINE_PATH + +CMD /fython exec "Shell.start()" $SHELL_PATH_COMPILED $DESTINE_PATH $ADITIONAL_PATHS FROM base as compiler ENV PROJET_FOLDER="/project" CMD echo "Compiling project: $PROJET_FOLDER" \ + && rm -rf $PROJET_FOLDER/_compiled \ && /fython exec "Core.Code.compile_project('$PROJET_FOLDER')" $DESTINE_PATH -FROM base as tests -COPY tests tests +FROM base as fytest + +ENV FOLDER="MUST_BE_GIVEN" + +ENV FYTEST_PATH="/libs/fytest" +ENV FYTEST_PATH_COMPILED="/libs/fytest/_compiled" + +COPY libs/fytest $FYTEST_PATH +RUN /fython exec "Core.Code.compile_project('$FYTEST_PATH')" $DESTINE_PATH -CMD /fython exec "Core.Code.compile_project('/tests')" $DESTINE_PATH \ - && /fython exec "Math_tests.run_tests()" $DESTINE_PATH /tests/_compiled +CMD echo "Compiling $FOLDER" \ + && /fython exec "Core.Code.compile_project('$FOLDER')" $DESTINE_PATH \ + && /fython exec "Fytest.run('$FOLDER')" $FOLDER/_compiled $FYTEST_PATH_COMPILED $DESTINE_PATH diff --git a/devtools/bootstrap.sh b/devtools/bootstrap.sh index 1183b7c..bd858dd 100755 --- a/devtools/bootstrap.sh +++ b/devtools/bootstrap.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e # stop if any error +set -eo pipefail # stop if any error compile () { # We use the _COMP sufix to avoid conflict with external variables @@ -8,19 +8,49 @@ compile () { DESTINE_PATH_COMP=$2 PATH_FYTHON_TO_USE_AS_BOOTSTRAPER_COMP=$3 ELIXIR_BEAMS_PATH_COMP=$4 + COMPILE_IN_PARALEL=true + + [[ -z "$SRC_DIR_COMP" ]] && echo "Missing src dir of fython" && exit 1 + [[ -z "$DESTINE_PATH_COMP" ]] && echo "Missing destine path of bootstrap" && exit 1 + [[ -z "$PATH_FYTHON_TO_USE_AS_BOOTSTRAPER_COMP" ]] && echo "Missing path of previous fython to use as bootstraper" && exit 1 + [[ -z "$ELIXIR_BEAMS_PATH_COMP" ]] && echo "Missing elixir beams path" && exit 1 ALL_FILES_PATH_COMP=$(find $SRC_DIR_COMP -name '*.fy') echo "Destine folder: $DESTINE_PATH_COMP" - rm -rf $DESTINE_PATH_COMP - mkdir $DESTINE_PATH_COMP + rm -rf $DESTINE_PATH_COMP/* cd $ELIXIR_BEAMS_PATH_COMP && cp -r . "$DESTINE_PATH_COMP" && cd / + EXIT_CODES_PATH=/tmp/bootstra_exit_codes + rm -rf $EXIT_CODES_PATH + mkdir $EXIT_CODES_PATH + for FILE_PATH in $ALL_FILES_PATH_COMP; do - ERL_COMMAND_CALL="application:start(compiler), application:start(elixir), 'Elixir.Code':compiler_options(#{ignore_module_conflict => true}), 'Fython.Core.Code':compile_project_file(<<"'"'$SRC_DIR_COMP'"'">>, <<"'"'${FILE_PATH}'"'">>, "'"'$DESTINE_PATH_COMP'"'"), init:stop()."; - erl -pa $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER_COMP -noshell -eval "$ERL_COMMAND_CALL"; + ERL_COMMAND_CALL="application:start(compiler), application:start(elixir), 'Elixir.Code':compiler_options(#{ignore_module_conflict => true}), 'Fython.Core.Code':compile_project_file(<<"'"'$SRC_DIR_COMP'"'">>, <<"'"'${FILE_PATH}'"'">>, "'"'$DESTINE_PATH_COMP'"'", true), init:stop()."; + FILE_PATH_SCAPED=$(echo $FILE_PATH | sed 's/\//SEPARATOR/g') + + if [ "$COMPILE_IN_PARALEL" = true ] ; then + erl -pa $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER_COMP -noshell -eval "$ERL_COMMAND_CALL" || echo $? > $EXIT_CODES_PATH/$FILE_PATH_SCAPED & + else + erl -pa $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER_COMP -noshell -eval "$ERL_COMMAND_CALL" || echo $? > $EXIT_CODES_PATH/$FILE_PATH_SCAPED + fi + done + wait + + # when running in parallel we need this to ensure that all comands returned 0 code + # otherwise this script would return 0 code even when some erl command failed + for RESULT in `ls $EXIT_CODES_PATH`; do + ORIGINAL_PATH=$(echo $RESULT | sed 's/SEPARATOR/\//g') + RESULT=$(cat $EXIT_CODES_PATH/$RESULT) + if [[ $RESULT -ne "0" ]]; then + echo "Bootstrap Failed --------------------" + echo "Failed to compile files: $ORIGINAL_PATH" + echo "-------------------------------------" + exit 1 + fi done + echo "Bootstraped with success" } $* diff --git a/devtools/bootstrap_old.sh b/devtools/bootstrap_old.sh new file mode 100755 index 0000000..3259a82 --- /dev/null +++ b/devtools/bootstrap_old.sh @@ -0,0 +1,52 @@ +#!/bin/bash +set -eo pipefail # stop if any error + +compile () { + # We use the _COMP sufix to avoid conflict with external variables + # (variabls of the shell that is running this script) + SRC_DIR_COMP=$1 + DESTINE_PATH_COMP=$2 + PATH_FYTHON_TO_USE_AS_BOOTSTRAPER_COMP=$3 + ELIXIR_BEAMS_PATH_COMP=$4 + COMPILE_IN_PARALEL=true + + ALL_FILES_PATH_COMP=$(find $SRC_DIR_COMP -name '*.fy') + + echo "Destine folder: $DESTINE_PATH_COMP" + rm -rf $DESTINE_PATH_COMP + mkdir $DESTINE_PATH_COMP + + cd $ELIXIR_BEAMS_PATH_COMP && cp -r . "$DESTINE_PATH_COMP" && cd / + + EXIT_CODES_PATH=/tmp/bootstra_exit_codes + rm -rf $EXIT_CODES_PATH + mkdir $EXIT_CODES_PATH + + for FILE_PATH in $ALL_FILES_PATH_COMP; do + ERL_COMMAND_CALL="application:start(compiler), application:start(elixir), 'Elixir.Code':compiler_options(#{ignore_module_conflict => true}), 'Fython.Core.Code':compile_project_file(<<"'"'$SRC_DIR_COMP'"'">>, <<"'"'${FILE_PATH}'"'">>, "'"'$DESTINE_PATH_COMP'"'"), init:stop()."; + FILE_PATH_SCAPED=$(echo $FILE_PATH | sed 's/\//SEPARATOR/g') + + if [ "$COMPILE_IN_PARALEL" = true ] ; then + erl -pa $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER_COMP -noshell -eval "$ERL_COMMAND_CALL" || echo $? > $EXIT_CODES_PATH/$FILE_PATH_SCAPED & + else + erl -pa $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER_COMP -noshell -eval "$ERL_COMMAND_CALL" || echo $? > $EXIT_CODES_PATH/$FILE_PATH_SCAPED + fi + done + wait + + # when running in parallel we need this to ensure that all comands returned 0 code + # otherwise this script would return 0 code even when some erl command failed + for RESULT in `ls $EXIT_CODES_PATH`; do + ORIGINAL_PATH=$(echo $RESULT | sed 's/SEPARATOR/\//g') + RESULT=$(cat $EXIT_CODES_PATH/$RESULT) + if [[ $RESULT -ne "0" ]]; then + echo "Bootstrap Failed --------------------" + echo "Failed to compile files: $ORIGINAL_PATH" + echo "-------------------------------------" + exit 1 + fi + done + echo "Bootstraped with success" +} + +$* diff --git a/devtools/fython b/devtools/fython index aca6925..c5ad560 100644 --- a/devtools/fython +++ b/devtools/fython @@ -1,5 +1,5 @@ #!/bin/bash -set -e # stop if any error +set -eo pipefail # stop if any error exec () { FYTHON_CODE=$1 @@ -9,7 +9,7 @@ exec () { for ERL_PATH in $ERLANG_PATHS; do ERLANG_PATHS_FORMATED=$ERLANG_PATHS_FORMATED"-pa $ERL_PATH " done - erl $ERLANG_PATHS_FORMATED -noshell -eval "'Fython.Core':eval_string(<<"'"'""'"'">>, <<"'"'"$FYTHON_CODE"'"'">>)." -run init stop + erl $ERLANG_PATHS_FORMATED -noshell -eval "'Fython.Core':eval_string(<<"'"'"$FYTHON_CODE"'"'">>)." -run init stop } $* \ No newline at end of file diff --git a/fy/fy/__init__.fy b/libs/fy/fy/__init__.fy similarity index 100% rename from fy/fy/__init__.fy rename to libs/fy/fy/__init__.fy diff --git a/fy/fy/core.fy b/libs/fy/fy/core.fy similarity index 100% rename from fy/fy/core.fy rename to libs/fy/fy/core.fy diff --git a/fy/fy/deps.fy b/libs/fy/fy/deps.fy similarity index 100% rename from fy/fy/deps.fy rename to libs/fy/fy/deps.fy diff --git a/fy_doc/fy_doc/__init__.fy b/libs/fy_doc/fy_doc/__init__.fy similarity index 100% rename from fy_doc/fy_doc/__init__.fy rename to libs/fy_doc/fy_doc/__init__.fy diff --git a/fy_doc/fy_doc/front/.browserslistrc b/libs/fy_doc/fy_doc/front/.browserslistrc similarity index 100% rename from fy_doc/fy_doc/front/.browserslistrc rename to libs/fy_doc/fy_doc/front/.browserslistrc diff --git a/fy_doc/fy_doc/front/.editorconfig b/libs/fy_doc/fy_doc/front/.editorconfig similarity index 100% rename from fy_doc/fy_doc/front/.editorconfig rename to libs/fy_doc/fy_doc/front/.editorconfig diff --git a/fy_doc/fy_doc/front/.eslintrc.js b/libs/fy_doc/fy_doc/front/.eslintrc.js similarity index 100% rename from fy_doc/fy_doc/front/.eslintrc.js rename to libs/fy_doc/fy_doc/front/.eslintrc.js diff --git a/fy_doc/fy_doc/front/.gitignore b/libs/fy_doc/fy_doc/front/.gitignore similarity index 100% rename from fy_doc/fy_doc/front/.gitignore rename to libs/fy_doc/fy_doc/front/.gitignore diff --git a/fy_doc/fy_doc/front/README.md b/libs/fy_doc/fy_doc/front/README.md similarity index 100% rename from fy_doc/fy_doc/front/README.md rename to libs/fy_doc/fy_doc/front/README.md diff --git a/fy_doc/fy_doc/front/babel.config.js b/libs/fy_doc/fy_doc/front/babel.config.js similarity index 100% rename from fy_doc/fy_doc/front/babel.config.js rename to libs/fy_doc/fy_doc/front/babel.config.js diff --git a/fy_doc/fy_doc/front/package-lock.json b/libs/fy_doc/fy_doc/front/package-lock.json similarity index 100% rename from fy_doc/fy_doc/front/package-lock.json rename to libs/fy_doc/fy_doc/front/package-lock.json diff --git a/fy_doc/fy_doc/front/package.json b/libs/fy_doc/fy_doc/front/package.json similarity index 100% rename from fy_doc/fy_doc/front/package.json rename to libs/fy_doc/fy_doc/front/package.json diff --git a/fy_doc/fy_doc/front/public/docs.json b/libs/fy_doc/fy_doc/front/public/docs.json similarity index 100% rename from fy_doc/fy_doc/front/public/docs.json rename to libs/fy_doc/fy_doc/front/public/docs.json diff --git a/fy_doc/fy_doc/front/public/index.html b/libs/fy_doc/fy_doc/front/public/index.html similarity index 100% rename from fy_doc/fy_doc/front/public/index.html rename to libs/fy_doc/fy_doc/front/public/index.html diff --git a/fy_doc/fy_doc/front/src/App.vue b/libs/fy_doc/fy_doc/front/src/App.vue similarity index 100% rename from fy_doc/fy_doc/front/src/App.vue rename to libs/fy_doc/fy_doc/front/src/App.vue diff --git a/fy_doc/fy_doc/front/src/components/Content.vue b/libs/fy_doc/fy_doc/front/src/components/Content.vue similarity index 100% rename from fy_doc/fy_doc/front/src/components/Content.vue rename to libs/fy_doc/fy_doc/front/src/components/Content.vue diff --git a/fy_doc/fy_doc/front/src/components/LeftBar/LeftBar.vue b/libs/fy_doc/fy_doc/front/src/components/LeftBar/LeftBar.vue similarity index 100% rename from fy_doc/fy_doc/front/src/components/LeftBar/LeftBar.vue rename to libs/fy_doc/fy_doc/front/src/components/LeftBar/LeftBar.vue diff --git a/fy_doc/fy_doc/front/src/components/RightBar.vue b/libs/fy_doc/fy_doc/front/src/components/RightBar.vue similarity index 100% rename from fy_doc/fy_doc/front/src/components/RightBar.vue rename to libs/fy_doc/fy_doc/front/src/components/RightBar.vue diff --git a/fy_doc/fy_doc/front/src/main.js b/libs/fy_doc/fy_doc/front/src/main.js similarity index 100% rename from fy_doc/fy_doc/front/src/main.js rename to libs/fy_doc/fy_doc/front/src/main.js diff --git a/fy_doc/fy_doc/front/src/router/index.js b/libs/fy_doc/fy_doc/front/src/router/index.js similarity index 100% rename from fy_doc/fy_doc/front/src/router/index.js rename to libs/fy_doc/fy_doc/front/src/router/index.js diff --git a/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Bold.ttf b/libs/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Bold.ttf similarity index 100% rename from fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Bold.ttf rename to libs/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Bold.ttf diff --git a/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Italic.ttf b/libs/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Italic.ttf similarity index 100% rename from fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Italic.ttf rename to libs/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Italic.ttf diff --git a/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Light.ttf b/libs/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Light.ttf similarity index 100% rename from fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Light.ttf rename to libs/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Light.ttf diff --git a/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Regular.ttf b/libs/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Regular.ttf similarity index 100% rename from fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Regular.ttf rename to libs/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-Regular.ttf diff --git a/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-SemiBold.ttf b/libs/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-SemiBold.ttf similarity index 100% rename from fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-SemiBold.ttf rename to libs/fy_doc/fy_doc/front/src/static/fonts/SourceSansPro-SemiBold.ttf diff --git a/fy_doc/fy_doc/front/src/static/general.css b/libs/fy_doc/fy_doc/front/src/static/general.css similarity index 100% rename from fy_doc/fy_doc/front/src/static/general.css rename to libs/fy_doc/fy_doc/front/src/static/general.css diff --git a/fy_doc/fy_doc/front/src/static/variables.css b/libs/fy_doc/fy_doc/front/src/static/variables.css similarity index 100% rename from fy_doc/fy_doc/front/src/static/variables.css rename to libs/fy_doc/fy_doc/front/src/static/variables.css diff --git a/fy_doc/fy_doc/front/src/utils.js b/libs/fy_doc/fy_doc/front/src/utils.js similarity index 100% rename from fy_doc/fy_doc/front/src/utils.js rename to libs/fy_doc/fy_doc/front/src/utils.js diff --git a/fy_doc/fy_doc/front/src/views/Home.vue b/libs/fy_doc/fy_doc/front/src/views/Home.vue similarity index 100% rename from fy_doc/fy_doc/front/src/views/Home.vue rename to libs/fy_doc/fy_doc/front/src/views/Home.vue diff --git a/fy_doc/requirements.txt b/libs/fy_doc/requirements.txt similarity index 100% rename from fy_doc/requirements.txt rename to libs/fy_doc/requirements.txt diff --git a/libs/fytest/__init__.fy b/libs/fytest/__init__.fy new file mode 100644 index 0000000..b7109d1 --- /dev/null +++ b/libs/fytest/__init__.fy @@ -0,0 +1,45 @@ +def run(path): + # TODO it should compile the tests module + + test_functions_per_module = Fytest.Discover.find_test_functions(path) + Elixir.IO.puts('Colleted tests:') + Elixir.IO.inspect(test_functions_per_module) + run_tests(test_functions_per_module) + +def run_tests(test_functions_per_module): + results_per_module = test_functions_per_module + |> Elixir.Enum.map(lambda (module, functions): + results = functions + |> Elixir.Enum.map( + lambda (test_name, arity): + execute_test(module, test_name, arity) + ) + (module, results) + ) + + results_per_module + |> Elixir.Enum.map(lambda (module, results): + show_results(results) + ) + +def show_results(results): + passed = results + |> Elixir.Enum.filter(lambda (status, _, _, _, _): status == :passed) + |> Elixir.Enum.map(lambda (_, module, test_name, _, _): + Elixir.IO.puts(Elixir.Enum.join(["PASSED: ", module, test_name])) + ) + + Elixir.IO.puts('=========================') + + failed = results + |> Elixir.Enum.filter(lambda (status, _, _, _, _): status == :failed) + |> Elixir.Enum.map(lambda (_, module, test_name, error, stacktrace): + Elixir.IO.puts(Elixir.Enum.join(["FAILED: ", module, test_name])) + ) + +def execute_test(module, test_name, _arity): + try: + Elixir.Kernel.apply(module, test_name, []) + (:passed, module, test_name, None, None) + except error: + (:failed, module, test_name, error, __STACKTRACE__) diff --git a/libs/fytest/discover/__init__.fy b/libs/fytest/discover/__init__.fy new file mode 100644 index 0000000..494d8c4 --- /dev/null +++ b/libs/fytest/discover/__init__.fy @@ -0,0 +1,40 @@ +def get_module_name_of_path(path): + path + |> Elixir.String.replace_suffix('/', '') + |> Elixir.String.split('/') + |> Elixir.Enum.at(-1) + |> Elixir.Macro.camelize() + +def find_test_functions(test_module): + modules = get_modules(test_module) + + modules + |> Elixir.Enum.map( + lambda module: + functions = module |> get_tests_of_module() + (module, functions) + ) + +def get_tests_of_module(module): + module + |> get_functions_of_module() + |> Elixir.Enum.filter(lambda (func_name, _arity): + func_name + |> Elixir.Atom.to_string() + |> Elixir.String.starts_with?("test_") + ) + +def get_functions_of_module(module): + # get_functions_of_module(:"Fython.Shell") + # TODO go to core + Elixir.Kernel.apply(module, :__info__, [:functions]) + +def get_modules(path): + [path, "**/*.fy"] + |> Elixir.Enum.join('/') + |> Elixir.Path.wildcard() + |> Elixir.Enum.map(lambda file_full_path: + module_name = Core.Code.get_module_name(path, file_full_path) + (:module, module) = Elixir.Code.ensure_loaded(Elixir.String.to_atom(module_name)) + module + ) diff --git a/src/shell/__init__.fy b/libs/shell/__init__.fy similarity index 78% rename from src/shell/__init__.fy rename to libs/shell/__init__.fy index de3bde6..0688e92 100644 --- a/src/shell/__init__.fy +++ b/libs/shell/__init__.fy @@ -1,4 +1,9 @@ def start(): + env = [] # where variables, etc will be saved + config = { + "file": "stdin", + } + start( 0, { @@ -6,10 +11,11 @@ def start(): 'last_output': None, 'current_command': '' # multiline command }, - [], + env, + config ) -def start(count, state, env): +def start(count, state, env, config): # to make space between lines is_multiline_command = state['current_command'] != '' @@ -31,7 +37,7 @@ def start(count, state, env): True -> Elixir.Enum.join([state['current_command'], '\n', user_input]) case: - user_input == "" -> start(count, state, env) + user_input == "" -> start(count, state, env, config) True -> first_char = Elixir.String.at(user_input, 0) last_char = Elixir.String.last(user_input) @@ -49,7 +55,7 @@ def start(count, state, env): case text: None -> raise "Line code not found" _ -> - (result, new_env) = execute(text, env) + (result, new_env) = execute(text, env, config) state = state |> Elixir.Map.merge({"last_output": result}) @@ -58,7 +64,7 @@ def start(count, state, env): state = Elixir.Map.merge(state, {'current_command': user_input}) (count, state, env) (not is_multiline_command) or (is_multiline_command and current_line == '') -> - (result, new_env) = execute(user_input, env) + (result, new_env) = execute(user_input, env, config) state = state |> Elixir.Map.merge({ @@ -72,16 +78,20 @@ def start(count, state, env): state = Elixir.Map.merge(state, {'current_command': user_input}) (count, state, env) - start(count, state, new_env) + start(count, state, new_env, config) -def execute(text, env): +def execute(text, env, config): try: - (result, new_env) = Core.eval_string('', text, env) - Elixir.IO.inspect(result) + config = Elixir.Map.put(config, 'env', env) + (result, new_env) = Core.eval_string('', text, config) + case result: + None -> None + _ -> Elixir.IO.inspect(result) + (result, new_env) except error: - # TODO this global handler for errors must be in the eval string code - Elixir.IO.puts("Exception") - Elixir.IO.puts(Elixir.Exception.format_stacktrace(__STACKTRACE__)) - Elixir.IO.inspect(__STACKTRACE__) + # usefull for debuggind + Elixir.IO.inspect("Shell recebeu o erro:") + Elixir.IO.inspect(error) +# Elixir.Kernel.reraise(error, __STACKTRACE__) (None, env) diff --git a/pretty_error2.py b/pretty_error2.py new file mode 100644 index 0000000..ade6256 --- /dev/null +++ b/pretty_error2.py @@ -0,0 +1,80 @@ +source_code = """def run(): + segunda() + +def segunda(): + da_erro(True) + +def da_erro(a): + b = 0 + + case a: + False -> None + 10 -> None + True -> 10 / b +""" + +meta = {"start": (16, 12, 16), "end": (22, 12, 22)} + +state = { + "source_code": source_code, + "source_code_lines": source_code.split('\n'), + "source_code_lines_indexed": list(enumerate(source_code.split('\n'))), + "line_num": meta['start'][1], # line starting at 0 + "start": (meta['start'][1], meta['start'][2]), + "end": (meta['end'][1], meta['end'][2]), +} + + +def get_lines_above(state): + HOW_MANY_LINES_SHOW_ABOVE = 5 + + source_code_lines = [(num, line) for num, line in enumerate(state['source_code_lines'])] + + if state['line_num'] > HOW_MANY_LINES_SHOW_ABOVE: + lines_to_keep_above = source_code_lines[slice(state['line_num']-HOW_MANY_LINES_SHOW_ABOVE, state['line_num'])] + else: + lines_to_keep_above = source_code_lines[0:state['line_num']] + + return lines_to_keep_above + + +def draw_pointers(state): + start, end = state['start'][1], state['end'][1] + + pointer = " " * (get_need_size_numbers(state) + 3) + for i in range(0, start): + pointer += '~' + + for i in range(start, end): + pointer += "^" + return [pointer] + + +def get_lines_to_print(state): + lines_above = get_lines_above(state) + current_line = state['source_code_lines_indexed'][state['line_num']] + pointers = draw_pointers(state) + + return [ + *add_line_numbers(meta, lines_above), + *add_line_numbers(meta, [current_line]), + *pointers, + ] + + +def get_need_size_numbers(state): + return len(str(state['end'][0])) + + +def add_line_numbers(meta, list_of_lines_indexed): + size = get_need_size_numbers(meta) + return [ + f"{str(i + 1).rjust(size)} ǁ {l}" for i, l in list_of_lines_indexed + ] + + +def print_error(state): + for line in get_lines_to_print(state): + print(line) + +print_error(state) \ No newline at end of file diff --git a/src/core/__init__.fy b/src/core/__init__.fy index 964874f..15184c8 100644 --- a/src/core/__init__.fy +++ b/src/core/__init__.fy @@ -1,27 +1,19 @@ -def eval_file(module_name, file_path): - text = Elixir.File.read(file_path) |> Elixir.Kernel.elem(1) +def eval_string(text): + eval_string('', text, {"file": ''}) - eval_string(module_name, text) +def eval_string(module_name, text, config): + (state, converted) = Core.Code.lexer_parse_convert_file(module_name, text, config) -def eval_string(module_name, text): - eval_string(module_name, text, []) - -def eval_string(module_name, text, env): - state_n_converted = Core.Code.lexer_parse_convert_file(module_name, "", text, env) - - state = Elixir.Enum.at(state_n_converted, 0) - converted = Elixir.Enum.at(state_n_converted, 1) + file = Elixir.Map.get(config, 'file') + env = Elixir.Map.get(config, 'env', []) case converted: None -> - Core.Errors.Utils.print_error('', state, text) + Core.Errors.Utils.print_error(file, state, text) (None, env) _ -> - Elixir.IO.inspect(converted) - - # (result, new_env) try: - converted |> Elixir.Code.eval_quoted(env) - except Elixir.CompileError as e: -# Elixir.IO.inspect(e) - (None, env) + Elixir.Code.eval_quoted(converted, env, []) + except error: +# Exceptions.format_traceback(error, __STACKTRACE__) + Elixir.Kernel.reraise(error, __STACKTRACE__) diff --git a/src/core/code.fy b/src/core/code.fy index 4861e1a..f8d5ed0 100644 --- a/src/core/code.fy +++ b/src/core/code.fy @@ -11,42 +11,25 @@ def compile_project(project_path, destine): # Ensure compiled folder is created Elixir.File.mkdir_p!(compiled_folder) - # Copy elixir beams to folder - copy_elixir_beams(compiled_folder) - [project_path, "**/*.fy"] |> Elixir.Enum.join('/') |> Elixir.Path.wildcard() |> Elixir.Enum.map(lambda file_full_path: - compile_project_file(project_path, file_full_path, compiled_folder) + compile_project_file(project_path, file_full_path, compiled_folder, False) ) - -def copy_elixir_beams(compiled_folder): - elixir_path = '/usr/lib/elixir/lib/elixir/ebin' - - case Elixir.File.exists?(elixir_path): - True -> Elixir.Enum.join([elixir_path, '*'], '/') - |> Elixir.Path.wildcard() - |> Elixir.Enum.each(lambda beam_file: - file_name = beam_file - |> Elixir.String.split('/') - |> Elixir.List.last() - - Elixir.File.cp!(beam_file, Elixir.Enum.join([compiled_folder, file_name], '/')) - ) - False -> :error - -def compile_project_file(project_root, file_full_path, destine_compiled): - module_name = get_module_name(project_root, file_full_path) +def compile_project_file(project_root, file_full_path, destine_compiled, bootstraping): + module_name = get_module_name(project_root, file_full_path, bootstraping) # Ensure compiled folder is created Elixir.File.mkdir_p!(destine_compiled) Elixir.IO.puts(Elixir.Enum.join(["Compiling module: ", module_name])) - [state, quoted] = lexer_parse_convert_file( - module_name, file_full_path, Elixir.File.read(file_full_path) |> Elixir.Kernel.elem(1) + (state, quoted) = lexer_parse_convert_file( + module_name, + Elixir.File.read(file_full_path) |> Elixir.Kernel.elem(1), + {"file": file_full_path, "compiling_module": True} ) case Elixir.Map.get(state, "error"): @@ -55,50 +38,67 @@ def compile_project_file(project_root, file_full_path, destine_compiled): # to ensure that our module binary will not have # Elixir. in the begin of the module name (_, _, binary, _) = Elixir.Module.create( - Elixir.String.to_atom(module_name), quoted, Elixir.Macro.Env.location(__ENV__) + Elixir.String.to_atom(module_name), + quoted, + [(:file, file_full_path)] ) - Elixir.File.write( - Elixir.Enum.join([destine_compiled, "/", module_name, ".beam"]), - binary, - mode=:binary - ) - Elixir.File.write( - Elixir.Enum.join([destine_compiled, "/", module_name, ".ex"]), - Elixir.Macro.to_string(quoted), - ) + # we save .ex just to help debugging + destine_ex = Elixir.Enum.join([destine_compiled, "/", module_name, ".ex"]) + destine_beam = Elixir.Enum.join([destine_compiled, "/", module_name, ".beam"]) + + Elixir.File.write(destine_ex, Elixir.Macro.to_string(quoted)) + Elixir.File.write(destine_beam, binary, mode=:binary) _ -> Elixir.IO.puts("Compilation error:") Elixir.IO.puts("file path:") Elixir.IO.puts(file_full_path) text = Elixir.File.read(file_full_path) |> Elixir.Kernel.elem(1) Core.Errors.Utils.print_error(module_name, state, text) - :error + raise "Compilation failed" -def lexer_parse_convert_file(module_name, files_full_path, text): - lexer_parse_convert_file(module_name, files_full_path, text, []) - -def lexer_parse_convert_file(module_name, files_full_path, text, env): +def lexer_parse_convert_file(module_name, text, config): lexed = Core.Lexer.execute(text) + # 1º Convert each node from Fython AST to Elixir AST state = case Elixir.Map.get(lexed, "error"): None -> tokens = Elixir.Map.get(lexed, "tokens") - Core.Parser.execute(files_full_path, tokens, env) + Core.Parser.execute(tokens, config) _ -> lexed - # 2º Convert each node from json to Fython format + state_error = Elixir.Map.get(state, 'error') + compiling_module = Elixir.Map.get(config, "compiling_module", False) + + # 2º Inject usefull functions into the module + state = case [state_error, compiling_module]: + [None, True] -> + node = Core.Parser.Pos.Nodesrefs.run(state['node'], text) + Elixir.Map.put(state, 'node', node) + _ -> state + +# Elixir.IO.inspect(state['node']) + + # 3º Convert each node from Fython AST to Elixir AST case Elixir.Map.get(state, 'error'): None -> ast = Elixir.Map.get(state, 'node') - [state, Core.Generator.Conversor.convert(ast)] - _ -> [state, None] + (state, Core.Generator.Conversor.convert(ast)) + _ -> (state, None) def get_module_name(project_full_path, file_full_path): - # input > /home/joao/fythonproject/module/utils.fy - # output > ModuleA.Utils + get_module_name(project_full_path, file_full_path, False) + + +def get_module_name(project_full_path, file_full_path, bootstraping): + # input > + # project_full_path = /home/joao/fythonproject + # file_full_path = /home/joao/fythonproject/module/utils.fy + # bootstraping (when True we will not add the name of marent folder to the module's name +# Otherwise Fython itself would have modules called Src, e.g Fython.Src.Core.func_name) + # output > Module.Utils # if file name is __init__.fy # we wil remove this name and the module name passes to be @@ -108,7 +108,16 @@ def get_module_name(project_full_path, file_full_path): True -> None False -> raise "File fullpath doent match project full path" - name = Elixir.String.replace_prefix(file_full_path, project_full_path, "") + project_parent_path = case bootstraping: + False -> + project_full_path + |> Elixir.String.replace_suffix('/', '') + |> Elixir.String.split('/') + |> Elixir.Enum.slice(0..-2) + |> Elixir.Enum.join('/') + True -> project_full_path + + name = Elixir.String.replace_prefix(file_full_path, project_parent_path, "") # remove / from the start of the name name = case Elixir.String.graphemes(name) |> Elixir.Enum.at(0): @@ -133,9 +142,3 @@ def get_module_name(project_full_path, file_full_path): _ -> final |> Elixir.Enum.join('.') Elixir.Enum.join(["Fython.", final]) - - -def parallel_map(collection, func): - collection - |> Elixir.Enum.map(lambda i: Elixir.Task.async(lambda: func(i))) - |> Elixir.Enum.map(lambda i: Elixir.Task.await(i, :infinity)) diff --git a/src/core/generator/Compiler.fy b/src/core/generator/Compiler.fy deleted file mode 100644 index 0b0d127..0000000 --- a/src/core/generator/Compiler.fy +++ /dev/null @@ -1,160 +0,0 @@ -def compile_project(project_path): - compile_project(project_path, "_compiled") - -def compile_project(project_path, destine): - compile_project(project_path, destine, False) - -def compile_project(project_path, destine, bootstrap): - compiled_folder = [project_path, destine] |> Elixir.Enum.join('/') - - # Ensure compiled folder is created - Elixir.File.mkdir_p!(compiled_folder) - - # Copy elixir beams to folder - copy_elixir_beams(compiled_folder) - - compile_project_to_binary(project_path, compiled_folder, bootstrap) - - -def copy_elixir_beams(compiled_folder): - elixir_path = '/usr/lib/elixir/lib/elixir/ebin' - - case Elixir.File.exists?(elixir_path): - True -> Elixir.Enum.join([elixir_path, '*'], '/') - |> Elixir.Path.wildcard() - |> Elixir.Enum.each(lambda beam_file: - file_name = beam_file - |> Elixir.String.split('/') - |> Elixir.List.last() - - Elixir.File.cp!(beam_file, Elixir.Enum.join([compiled_folder, file_name], '/')) - ) - False -> :error - -def compile_project_to_binary(directory_path, compiled_folder, bootstrap): - # Return a list of each module compiled into elixir AST in binary - # read to be evaluated in Elixir - # Ex: - # iex> Compiler.compile_project(ABSOLUTE_PATH_TO_PROJECT_FOLDER) - # exit the shell - # go to compiled folder and start iex again. - # Now, you should be able to call any module of this project in iex - - files_path = [directory_path, "**/*.fy"] - |> Elixir.Enum.join('/') - |> Elixir.Path.wildcard() - |> Elixir.Enum.sort_by(lambda full_path: - case bootstrap: - True -> - # Nowdays, our compiler evaluate the code when finishes - # the compilation. It can cause some problems when doing - # a bootstrap. We try to mitigate it putting the most important - # core modules to be compiled at the end of the process if its - # a bootstrap. This is a temporary fix. We may want to - # compile a module without ever evaluate it. - - current_folder_parent = Elixir.File.cwd!() - |> Elixir.String.split("/") - |> Elixir.Enum.split(-1) - |> elem(0) - |> Elixir.Enum.join("/") - - local_path = Elixir.String.replace(full_path, current_folder_parent, "") - - case: - Elixir.String.starts_with?(local_path, "/core/parser/__init__.fy") -> 1 - Elixir.String.starts_with?(local_path, "/core/generator/Compiler.fy") -> 1 - Elixir.String.starts_with?(local_path, "/core/generator/Conversor.fy") -> 2 - True -> 0 - False -> 0 - ) - - files_path - |> Elixir.Enum.map(lambda full_path: - module_name = get_module_name(directory_path, full_path) - - Elixir.IO.puts(Elixir.Enum.join(["Compiling module: ", module_name])) - - [state, quoted] = lexer_parse_convert_file( - module_name, full_path, Elixir.File.read(full_path) |> Elixir.Kernel.elem(1) - ) - - case Elixir.Map.get(state, "error"): - None -> - # Its super important to use this Module.create function - # to ensure that our module binary will not have - # Elixir. in the begin of the module name - (_, _, binary, _) = Elixir.Module.create( - Elixir.String.to_atom(module_name), quoted, [(:file, full_path), (:line, 0)] - ) - - Elixir.File.write( - Elixir.Enum.join([compiled_folder, "/", module_name, ".beam"]), - binary, - mode=:binary - ) - _ -> - Elixir.IO.puts("Compilation error:") - Elixir.IO.puts("file path:") - Elixir.IO.puts(full_path) - text = Elixir.File.read(full_path) |> Elixir.Kernel.elem(1) - Core.Errors.Utils.print_error(module_name, state, text) - :error - ) - - -def lexer_parse_convert_file(module_name, files_full_path, text): - lexer_parse_convert_file(module_name, files_full_path, text, []) - -def lexer_parse_convert_file(module_name, files_full_path, text, env): - lexed = Core.Lexer.execute(text) - - state = case Elixir.Map.get(lexed, "error"): - None -> - tokens = Elixir.Map.get(lexed, "tokens") - Core.Parser.execute(files_full_path, tokens, env) - _ -> - lexed - - - ast = Elixir.Map.get(state, 'node') - - # 2º Convert each node from json to Fython format - case Elixir.Map.get(state, 'error'): - None -> [state, Core.Generator.Conversor.convert(ast)] - _ -> [state, None] - - -def get_module_name(project_full_path, module_full_path): - # input > /home/joao/fythonproject/module/utils.fy - # output > ModuleA.Utils - - # if file name is __init__.fy - # we wil remove this name and the module name passes to be - # the name of this file parent folder - - regex = Elixir.Regex.compile(Elixir.Enum.join(["^", project_full_path, "/"])) - |> Elixir.Kernel.elem(1) - - name = Elixir.Regex.replace(regex, module_full_path, "") - |> Elixir.String.replace("/", ".") - - final = Elixir.Regex.compile("\.fy$") |> Elixir.Kernel.elem(1) - |> Elixir.Regex.replace(name, "") - |> Elixir.String.split('.') - |> Elixir.Enum.map(lambda i: Elixir.String.capitalize(i)) - - final = case final |> Elixir.List.last(): - "__init__" -> - final - |> Elixir.List.pop_at(-1) - |> Elixir.Kernel.elem(1) - |> Elixir.Enum.join('.') - _ -> final |> Elixir.Enum.join('.') - - Elixir.Enum.join(["Fython.", final]) - -def parallel_map(collection, func): - collection - |> Elixir.Enum.map(lambda i: Elixir.Task.async(lambda: func(i))) - |> Elixir.Enum.map(lambda i: Elixir.Task.await(i, :infinity)) diff --git a/src/core/generator/Conversor.fy b/src/core/generator/Conversor.fy index bdea1b7..1070bcb 100644 --- a/src/core/generator/Conversor.fy +++ b/src/core/generator/Conversor.fy @@ -1,30 +1,41 @@ -def convert_meta({"start": (_coll, line, _index), "file": file}): - [(:line, line + 1), (:file, file)] +def convert_meta((nodetype, {"ref_line": line}, body)): + # We are compiling a module + # Se we will use line numbers as keys, that we can later + # get the full meta of the node. + # We need this because elixir __STACKTRACE__ only inform us the line + # and to give users better errors messages, we need + # the position of tokens (eg. column in the curent line, etc) + # that we have in original meta + (nodetype, [(:line, line)], body) + +def convert_meta((nodetype, {"start": (_coll, line, _index)}, body)): + meta = [(:line, line + 1)] + (nodetype, meta, body) def convert(node): case Elixir.Kernel.elem(node, 0): - :number -> convert_number(node) - :atom -> convert_atom(node) - :var -> convert_var(node) - :string -> convert_string(node) - :unary -> convert_unary(node) - :list -> convert_list(node) - :tuple -> convert_tuple(node) - :binop -> convert_binop(node) - :pattern -> convert_pattern(node) - :if -> convert_if(node) - :func -> convert_func(node) - :statements -> convert_statements(node) - :lambda -> convert_lambda(node) - :def -> convert_def(node) - :static_access -> convert_static_access(node) - :raise -> convert_raise(node) - :pipe -> convert_pipe(node) - :map -> convert_map(node) - :case -> convert_case(node) - :call -> convert_call(node) - :try -> convert_try(node) - :range -> convert_range_node(node) + :number -> node |> convert_meta() |> convert_number() + :atom -> node |> convert_meta() |> convert_atom() + :var -> node |> convert_meta() |> convert_var() + :string -> node |> convert_meta() |> convert_string() + :unary -> node |> convert_meta() |> convert_unary() + :list -> node |> convert_meta() |> convert_list() + :tuple -> node |> convert_meta() |> convert_tuple() + :binop -> node |> convert_meta() |> convert_binop() + :pattern -> node |> convert_meta() |> convert_pattern() + :if -> node |> convert_meta() |> convert_if() + :func -> node |> convert_meta() |> convert_func() + :statements -> node |> convert_meta() |> convert_statements() + :lambda -> node |> convert_meta() |> convert_lambda() + :def -> node |> convert_meta() |> convert_def() + :static_access -> node |> convert_meta() |> convert_static_access() + :raise -> node |> convert_meta() |> convert_raise() + :pipe -> node |> convert_meta() |> convert_pipe() + :map -> node |> convert_meta() |> convert_map() + :case -> node |> convert_meta() |> convert_case() + :call -> node |> convert_meta() |> convert_call() + :try -> node |> convert_meta() |> convert_try() + :range -> node |> convert_meta() |> convert_range_node() def convert_number((:number, _, [value])): value @@ -42,23 +53,22 @@ def convert_var((:var, _, [_pinned, "None"])): None def convert_var((:var, meta, [True, value])): - (:"^", convert_meta(meta), [(Elixir.String.to_atom(value), convert_meta(meta), :Elixir)]) + (:"^", meta, [(Elixir.String.to_atom(value), meta, :Elixir)]) def convert_var((:var, meta, [False, value])): - (Elixir.String.to_atom(value), convert_meta(meta), :Elixir) + (Elixir.String.to_atom(value), meta, :Elixir) def convert_string((:string, meta, [value])): - value = Elixir.Enum.join(['"', value,'"']) |> Elixir.Code.eval_string() |> Elixir.Kernel.elem(0) - (:"<<>>", convert_meta(meta), [value]) + value def convert_unary((:unary, meta, [:minus, node])): - (:"-", convert_meta(meta), [convert(node)]) + (:"-", meta, [convert(node)]) def convert_unary((:unary, meta, [:plus, node])): - (:"+", convert_meta(meta), [convert(node)]) + (:"+", meta, [convert(node)]) def convert_unary((:unary, meta, [:not, node])): - (:"__block__", convert_meta(meta), [(:"!", convert_meta(meta), [convert(node)])]) + (:"__block__", meta, [(:"!", meta, [convert(node)])]) def is_unpack((:unpack, _, _)): True @@ -94,10 +104,10 @@ def convert_list_with_unpack((:list, meta, elements)): ( ( :".", - convert_meta(meta), + meta, [(:__aliases__, [(:alias, False)], [Elixir.String.to_atom("Elixir.Enum")]), :concat] ), - convert_meta(meta), + meta, [acc, item] ) ) @@ -108,19 +118,19 @@ def convert_list(node <- (:list, meta, elements)): _ -> convert_list_with_unpack(node) def convert_tuple((:tuple, meta, elements)): - (:"{}", convert_meta(meta), Elixir.Enum.map(elements, &convert/1)) + (:"{}", meta, Elixir.Enum.map(elements, &convert/1)) def convert_binop((:binop, meta, [left, :or, right])): - (:or, convert_meta(meta), [convert(left), convert(right)]) + (:or, meta, [convert(left), convert(right)]) def convert_binop((:binop, meta, [left, :and, right])): - (:and, convert_meta(meta), [convert(left), convert(right)]) + (:and, meta, [convert(left), convert(right)]) def convert_binop((:binop, meta, [left, :in, right])): - (:in, convert_meta(meta), [convert(left), convert(right)]) + (:in, meta, [convert(left), convert(right)]) def convert_binop((:binop, meta, [left, :pow, right])): - ((:".", convert_meta(meta), [:math, :pow]), convert_meta(meta), [convert(left), convert(right)]) + ((:".", meta, [:math, :pow]), meta, [convert(left), convert(right)]) def convert_binop((:binop, meta, [left, op, right])): elixir_op = { @@ -129,19 +139,19 @@ def convert_binop((:binop, meta, [left, op, right])): :ee: '==', :ne: '!=', :in: 'in' } - (Elixir.String.to_atom(elixir_op[op]), convert_meta(meta), [convert(left), convert(right)]) + (Elixir.String.to_atom(elixir_op[op]), meta, [convert(left), convert(right)]) def convert_pattern((:pattern, meta, [left, right])): - (:"=", convert_meta(meta), [convert(left), convert(right)]) + (:"=", meta, [convert(left), convert(right)]) def convert_if((:if, meta, [comp_expr, true_case, false_case])): - (:if, convert_meta(meta), [convert(comp_expr), [(:do, convert(true_case)), (:else, convert(false_case))]]) + (:if, meta, [convert(comp_expr), [(:do, convert(true_case)), (:else, convert(false_case))]]) def convert_func((:func, meta, [name, arity])): ( :"&", - convert_meta(meta), - [(:"/", convert_meta(meta), [(Elixir.String.to_atom(name), convert_meta(meta), :Elixir), arity])] + meta, + [(:"/", meta, [(Elixir.String.to_atom(name), meta, :Elixir), arity])] ) def convert_statements((:statements, meta, nodes)): @@ -149,34 +159,34 @@ def convert_statements((:statements, meta, nodes)): case Elixir.Enum.count(content): 1 -> Elixir.Enum.at(content, 0) - _ -> (:"__block__", convert_meta(meta), content) + _ -> (:"__block__", meta, content) def convert_lambda((:lambda, meta, [args, statements])): args = args |> Elixir.Enum.map(&convert/1) - (:fn, convert_meta(meta), [(:"->", convert_meta(meta), [args, convert(statements)])]) + (:fn, meta, [(:"->", meta, [args, convert(statements)])]) def convert_def((:def, meta, [name, args, statements])): args = Elixir.Enum.map(args, &convert/1) ( :def, - convert_meta(meta), + meta, [ - (Elixir.String.to_atom(name), convert_meta(meta), args), + (Elixir.String.to_atom(name), meta, args), [(:do, convert(statements))] ] ) def convert_static_access((:static_access, meta, [node_to_access, node_key])): ( - (:".", convert_meta(meta), [(:"__aliases__", [(:alias, False)], [:Map]), :fetch!]), - convert_meta(meta), + (:".", meta, [(:"__aliases__", [(:alias, False)], [:Map]), :fetch!]), + meta, [convert(node_to_access), convert(node_key)] ) def convert_raise((:raise, meta, [expr])): - (:raise, convert_meta(meta), [convert(expr)]) + (:raise, meta, [convert(expr)]) def get_childs((:pipe, _, [left_node, right_node])): [get_childs(left_node), get_childs(right_node)] @@ -237,7 +247,7 @@ def convert_map_with_spread((:map, meta, pairs)): (:spread, meta, [node_to_spread]) -> convert(node_to_spread) _ -> ( :'%{}', - convert_meta(meta), + meta, Elixir.Enum.map( pairs_or_spread, lambda (key, value): (convert(key), convert(value)) @@ -249,10 +259,10 @@ def convert_map_with_spread((:map, meta, pairs)): ( ( :".", - convert_meta(meta), + meta, [(:__aliases__, [(:alias, False)], [Elixir.String.to_atom("Elixir.Map")]), :merge] ), - convert_meta(meta), + meta, [acc, item] ) ) @@ -265,19 +275,19 @@ def convert_map(node <- (:map, meta, pairs)): (convert(key), convert(value)) ) - (:'%{}', convert_meta(meta), pairs) + (:'%{}', meta, pairs) _ -> convert_map_with_spread(node) def convert_case((:case, meta, [expr, pairs])): pairs = pairs |> Elixir.Enum.map(lambda (left, right): - (:"->", convert_meta(meta), [[convert(left)], convert(right)]) + (:"->", meta, [[convert(left)], convert(right)]) ) case expr: - None -> (:cond, convert_meta(meta), [[(:do, pairs)]]) - _ -> (:case, convert_meta(meta), [convert(expr), [(:do, pairs)]]) + None -> (:cond, meta, [[(:do, pairs)]]) + _ -> (:case, meta, [convert(expr), [(:do, pairs)]]) def convert_call_args(args, keywords): @@ -295,8 +305,8 @@ def convert_call((:call, meta, [node_to_call, args, keywords, True])): arguments = convert_call_args(args, keywords) ( - (:".", convert_meta(meta), [convert(node_to_call)]), - convert_meta(meta), + (:".", meta, [convert(node_to_call)]), + meta, arguments ) @@ -326,14 +336,14 @@ def convert_call(full <- (:call, meta, [(:var, _, [_, func_name]), args, keyword module = Elixir.String.to_atom(module) ( - (:".", convert_meta(meta), [module, Elixir.String.to_atom(function)]), - convert_meta(meta), + (:".", meta, [module, Elixir.String.to_atom(function)]), + meta, arguments ) False -> # this is for call a function that is defined in # the same module - (Elixir.String.to_atom(func_name), convert_meta(meta), arguments) + (Elixir.String.to_atom(func_name), meta, arguments) def convert_try((:try, meta, [try_block, exceptions, finally_block])): do = (:do, convert(try_block)) @@ -351,20 +361,9 @@ def convert_try((:try, meta, [try_block, exceptions, finally_block])): # > except error: ( :"->", - convert_meta(meta), - [ - [(Elixir.String.to_atom(except_identifier), convert_meta(meta), :Elixir)], - convert(block) - ] - ) - (False, None) -> - # Case of: - # > except ArithmeticError: - ( - :"->", - convert_meta(meta), + meta, [ - [(:'__aliases__', [(:alias, False)], [Elixir.String.to_atom(except_identifier)])], + [(Elixir.String.to_atom(except_identifier), meta, :Elixir)], convert(block) ] ) @@ -373,14 +372,14 @@ def convert_try((:try, meta, [try_block, exceptions, finally_block])): # > except ArithmeticError as error: ( :"->", - convert_meta(meta), + meta, [ [ ( :in, - convert_meta(meta), + meta, [ - (Elixir.String.to_atom(alias), convert_meta(meta), :Elixir), + (Elixir.String.to_atom(alias), meta, :Elixir), (:'__aliases__', [(:alias, False)], [Elixir.String.to_atom(except_identifier)]) ] ) @@ -392,15 +391,15 @@ def convert_try((:try, meta, [try_block, exceptions, finally_block])): rescue = (:rescue, each_rescue) - (:try, convert_meta(meta), [[do, rescue]]) + (:try, meta, [[do, rescue]]) def convert_range_node((:range, meta, [left_node, right_node])): ( ( :".", - convert_meta(meta), + meta, [(:__aliases__, [(:alias, False)], [Elixir.String.to_atom("Elixir.Range")]), :new] ), - convert_meta(meta), + meta, [convert(left_node), convert(right_node)] ) diff --git a/src/core/lexer/__init__.fy b/src/core/lexer/__init__.fy index 8f07963..5d3ad9a 100644 --- a/src/core/lexer/__init__.fy +++ b/src/core/lexer/__init__.fy @@ -6,7 +6,8 @@ def execute(text): "current_ident_level": 0, "error": None, "current_char": None, - "tokens": [] + "tokens": [], + "being_scaped": False } # Theres tokens that are multiline, like multiline string @@ -108,13 +109,15 @@ def advance(state): ln = state["position"]["ln"] col = state["position"]["col"] text = state["text"] + being_scaped = state["being_scaped"] prev_position = state['position'] + being_scaped = being_scaped == False and idx > 0 and Elixir.String.at(text, idx) == "\\" + idx = idx + 1 current_char = Elixir.String.at(text, idx) next_char = Elixir.String.at(text, idx + 1) - new_pos = case current_char == '\n': True -> position(idx, ln + 1, -1) False -> position(idx, ln, col + 1) @@ -123,7 +126,8 @@ def advance(state): "position": new_pos, "prev_position": prev_position, "current_char": current_char, - "next_char": next_char + "next_char": next_char, + "being_scaped": being_scaped } Elixir.Map.merge(state, new_state) @@ -214,7 +218,7 @@ def expected_double_maker(st, first, type, expected): def make_ident(state): first_char = state["current_char"] - state = loop_while(state, lambda cc, _: + state = loop_while(state, lambda cc, _, _: cc != None and cc == " " ) @@ -229,14 +233,36 @@ def make_ident(state): def loop_while(st, func): st = advance(st) cc = st["current_char"] + being_scaped = st["being_scaped"] result = Elixir.Map.get(st, "result") - valid = func(cc, st['next_char']) + result_callback = func(cc, st['next_char'], being_scaped) - case valid: + case result_callback: True -> Elixir.Map.put(st, "result", Elixir.Enum.join([result, cc])) |> loop_while(func) False -> st - _ -> set_error(st, valid) + :skip_cc -> loop_while(st, func) + _ -> set_error(st, result_callback) + +def loop_while_new(st, func): + st = advance(st) + cc = st["current_char"] + being_scaped = st["being_scaped"] + result = Elixir.Map.get(st, "result") + + [continue, value] = func(cc, st['next_char'], being_scaped) + + case [continue, value]: + [True, None] -> + loop_while_new(st, func) + [:skip_next, _] -> + st = Elixir.Map.put(st, "result", Elixir.Enum.join([result, value])) + loop_while_new(advance(st), func) + [True, _] -> + st = Elixir.Map.put(st, "result", Elixir.Enum.join([result, value])) + loop_while_new(st, func) + [False, _] -> st + _ -> set_error(st, value) def loop_until_sequence(state, expected_seq): @@ -283,7 +309,7 @@ def make_do_or_atom(state): state = state |> Elixir.Map.put("result", state["current_char"]) - |> loop_while(lambda cc, _: + |> loop_while(lambda cc, _, _: case is_atom_of_string: True -> cc != first_char False -> cc != None and Elixir.String.contains?(Core.Lexer.Consts.atom_chars(False), cc) @@ -325,8 +351,14 @@ def make_string(state): "MULLINESTRING", result, pos_start ) False -> - state = loop_while(state, lambda cc, _: - cc != string_char_type and cc != None + state = loop_while_new(state, lambda cc, nc, being_scaped: + case: + cc == "\\" and being_scaped -> [True, None] + nc == "n" and cc == "\\" and being_scaped == False -> [:skip_next, "\n"] + nc == '"' and cc == "\\" and being_scaped == False -> [:skip_next, '"'] + cc == string_char_type and being_scaped == True -> [True, cc] + cc == string_char_type and being_scaped == False -> [False, cc] + True -> [True, cc] ) # to advance the end string char @@ -334,11 +366,11 @@ def make_string(state): string = state |> Elixir.Map.get("result", "") - |> Elixir.String.graphemes() - |> Elixir.Enum.map(lambda i: - Elixir.Enum.join(['\\', '"']) if i == '"' else i - ) - |> Elixir.Enum.join() +# |> Elixir.String.graphemes() +# |> Elixir.Enum.map(lambda i: +# Elixir.Enum.join(['\\', '"']) if i == '"' else i +# ) +# |> Elixir.Enum.join() state |> Core.Lexer.Tokens.add_token( @@ -349,7 +381,7 @@ def make_string(state): def skip_comment(state): state = advance(state) - state = loop_while(state, lambda cc, _: + state = loop_while(state, lambda cc, _, _: cc != '\n' and cc != None ) Elixir.Map.delete(state, "result") @@ -358,7 +390,7 @@ def make_number(state): pos_start = state["position"] first_number = state["current_char"] - state = loop_while(state, lambda cc, nc: + state = loop_while(state, lambda cc, nc, _: valid_num_char = case cc: None -> False _ -> Elixir.String.contains?(Elixir.Enum.join([Core.Lexer.Consts.digists(), '._']), cc) @@ -391,7 +423,7 @@ def make_identifier(state): pos_start = state["position"] first_char = state["current_char"] - state = loop_while(state, lambda cc, nc: + state = loop_while(state, lambda cc, nc, _: cc != None and Elixir.String.contains?(Core.Lexer.Consts.identifier_chars(False), cc) and not (cc == "." and nc == ".") ) diff --git a/src/core/parser/__init__.fy b/src/core/parser/__init__.fy index e52fc3e..7f1970d 100644 --- a/src/core/parser/__init__.fy +++ b/src/core/parser/__init__.fy @@ -1,6 +1,12 @@ -def execute(file, tokens, env): +def execute(tokens, config): + # config: { + # "file": path, + # "env": [], # (optional) current defined variables, used for pos parser + # "compiling_module": bool # (optional) + # } + state = { - "file": file, + "file": config['file'], "error": None, "prev_tok": None, "current_tok": None, @@ -11,7 +17,8 @@ def execute(file, tokens, env): "_tokens": tokens |> Elixir.Enum.filter(lambda i: i["type"] != 'NEWLINE') } - state |> advance() |> parse() |> Core.Parser.Pos.execute(env) + result = state |> advance() |> parse() |> Core.Parser.Pos.execute(config) + def advance(state): # before anything, lets check that the states only contains expected keys @@ -670,7 +677,7 @@ def case_expr(state): is_cond = (state["current_tok"]["type"]) == "DO" - [state, _expr] = case is_cond: + [state, case_expr] = case is_cond: True -> [state, None] False -> expr(state) @@ -709,8 +716,25 @@ def case_expr(state): [state, left_expr] = expr(state) - case (state['current_tok']['type']) == 'ARROW': - True -> + state = case [case_expr, left_expr]: + [None, (:var, _, [_, "_"])] -> + Core.Parser.Utils.set_error( + state, + "If you want a clause to always match, you should use: True", + state["prev_tok"]["pos_start"], + state["prev_tok"]["pos_start"], + ) + [None, (:var, _, [_, "False"])] -> + Core.Parser.Utils.set_error( + state, + "False is not allowed here as it will never matchs", + state["prev_tok"]["pos_start"], + state["prev_tok"]["pos_end"], + ) + _ -> state + + case [state['error'],(state['current_tok']['type']) == 'ARROW']: + [None, True] -> state = advance(state) [state, right_expr] = case (state['current_tok']['ident']) == this_ident: @@ -720,13 +744,14 @@ def case_expr(state): cases = [*cases, (left_expr, right_expr)] state |> Elixir.Map.put('_cases', cases) - False -> + [None, False] -> Core.Parser.Utils.set_error( state, "Expected '->'", state["current_tok"]["pos_start"], state["current_tok"]["pos_end"] ) + [_, _] -> state ) False -> Core.Parser.Utils.set_error( @@ -751,7 +776,7 @@ def case_expr(state): state = Elixir.Map.delete(state, '_cases') node = Core.Parser.Nodes.make_case_node( - state['file'], _expr, cases, pos_start, state['current_tok']['pos_start'] + state['file'], case_expr, cases, pos_start, state['current_tok']['pos_start'] ) [state, node] diff --git a/src/core/parser/functions.fy b/src/core/parser/functions.fy index 986886f..ec4d504 100644 --- a/src/core/parser/functions.fy +++ b/src/core/parser/functions.fy @@ -48,9 +48,9 @@ def func_def_expr(state): True -> advance(state) False -> Core.Parser.Utils.set_error( state, - "Expected ':'", - state["current_tok"]["pos_start"], - state["current_tok"]["pos_end"] + "Missing ':' after function definition", + state["prev_tok"]["pos_end"], + state["prev_tok"]["pos_end"] ) state = case (state['current_tok']['pos_start']['ln']) > def_token_ln: @@ -79,7 +79,6 @@ def func_def_expr(state): case [arg_nodes, body]: [_, None] -> [state, None] [None, _] -> [state, None] - [None, None] -> [state, None] _ -> node = Core.Parser.Nodes.make_funcdef_node( state['file'], var_name_tok, arg_nodes, body, docstring, pos_start @@ -113,7 +112,6 @@ def lambda_expr(state): case [arg_nodes, body]: [_, None] -> [state, None] [None, _] -> [state, None] - [None, None] -> [state, None] _ -> node = Core.Parser.Nodes.make_lambda_node( state['file'], None, arg_nodes, body, pos_start diff --git a/src/core/parser/pos/__init__.fy b/src/core/parser/pos/__init__.fy index 5e07245..516c8de 100644 --- a/src/core/parser/pos/__init__.fy +++ b/src/core/parser/pos/__init__.fy @@ -1,14 +1,22 @@ -def execute(state, env): +def execute(state, config): + env = Elixir.Map.get(config, "env", []) + var_names_avaliable = env |> Elixir.Enum.map(lambda ((var_name, _), obj): Elixir.Atom.to_string(var_name) ) - case Elixir.Map.get(state, 'error'): - None -> + state_error = Elixir.Map.get(state, 'error') + compiling_module = Elixir.Map.get(config, "compiling_module", False) + + case [state_error, compiling_module]: + [None, True] -> node = state |> Elixir.Map.get('node') |> Core.Parser.Pos.Localcalls.convert_local_function_calls(var_names_avaliable) + # TODO remover isso do pos parser, isso só se aplica para compilação de módulos + # TODO pos parser deveria rodar sempre após o lex e parser + # |> Core.Parser.Pos.Nodesrefs.run(config) Elixir.Map.put(state, 'node', node) _ -> state diff --git a/src/core/parser/pos/localcalls.fy b/src/core/parser/pos/localcalls.fy index 727125d..1e2609c 100644 --- a/src/core/parser/pos/localcalls.fy +++ b/src/core/parser/pos/localcalls.fy @@ -152,19 +152,6 @@ def new_resolver((:statements, meta, nodes), var_names_avaliable): ) ) -def get_vars_defined_def_or_lambda(args, statements,var_names_avaliable): - received_arguments = args - |> Elixir.Enum.filter(lambda (node_type, _, _): - node_type in Core.Parser.Nodes.node_types_accept_pattern() - ) - |> Elixir.Enum.map(&get_variables_bound_in_pattern/1) - - statements = convert_local_function_calls( - statements, - Elixir.List.flatten(var_names_avaliable, received_arguments) - ) - - [args, statements] def new_resolver((:def, meta, [name, args, statements]), var_names_avaliable): [args, statements] = get_vars_defined_def_or_lambda( @@ -199,17 +186,6 @@ def new_resolver((:pipe, meta, [left_node, right_node]), var_names_avaliable): ] ) -def resolve_map_pair((key, value), var_names_avaliable): - ( - convert_local_function_calls(key, var_names_avaliable), - convert_local_function_calls(value, var_names_avaliable) - ) - -def resolve_map_pair((:spread, meta, [node_to_spread]), var_names_avaliable): - ( - :spread, meta, [new_resolver(node_to_spread, var_names_avaliable)] - ) - def new_resolver((:map, meta, pairs), var_names_avaliable): ( :map, @@ -257,4 +233,30 @@ def new_resolver(node <- (:range, meta, [left_node, right_node]), var_names_aval new_resolver(left_node, var_names_avaliable), new_resolver(right_node, var_names_avaliable), ] - ) \ No newline at end of file + ) + +def resolve_map_pair((key, value), var_names_avaliable): + ( + convert_local_function_calls(key, var_names_avaliable), + convert_local_function_calls(value, var_names_avaliable) + ) + +def resolve_map_pair((:spread, meta, [node_to_spread]), var_names_avaliable): + ( + :spread, meta, [new_resolver(node_to_spread, var_names_avaliable)] + ) + + +def get_vars_defined_def_or_lambda(args, statements,var_names_avaliable): + received_arguments = args + |> Elixir.Enum.filter(lambda (node_type, _, _): + node_type in Core.Parser.Nodes.node_types_accept_pattern() + ) + |> Elixir.Enum.map(&get_variables_bound_in_pattern/1) + + statements = convert_local_function_calls( + statements, + Elixir.List.flatten(var_names_avaliable, received_arguments) + ) + + [args, statements] \ No newline at end of file diff --git a/src/core/parser/pos/nodesrefs.fy b/src/core/parser/pos/nodesrefs.fy new file mode 100644 index 0000000..661ea86 --- /dev/null +++ b/src/core/parser/pos/nodesrefs.fy @@ -0,0 +1,204 @@ +def run(node, source_code): + # It has the job of converting the line of each node to an id. + # Elixir dont allow us to get more than the line number in __STACKTRACE__, + # so we use the line of node as a key and store the meta that we want + # in a function that we can call later with the line number and get the original meta + + state = {"node_count": 0, "meta_per_line_ref": {}} + + [node, state] = add_statements_refs(node, state) + + node + |> inject_into_node_quoted_function(quoted_get_refs_func(state['meta_per_line_ref'])) + |> inject_into_node_quoted_function(quoted_source_code_func(source_code)) + +def iterate_items(nodes, state): + acc = {"nodes": [], "state": state} + acc = Elixir.Enum.reduce(nodes, acc, lambda x, acc: + [x, state] = add_statements_refs(x, acc['state']) + {"nodes": Elixir.Enum.concat(acc['nodes'], [x]), "state": state} + ) + [acc['nodes'], acc['state']] + + +def add_statements_refs((left, right), state): + [left, state] = add_statements_refs(left, state) + [right, state] = add_statements_refs(right, state) + [(left, right), state] + +def add_statements_refs([node], state): + [node, state] = add_statements_refs(node, state) + [[node], state] + +def add_statements_refs(False, state): + [False, state] + +def add_statements_refs(True, state): + [True, state] + +def add_statements_refs(None, state): + [None, state] + +def add_statements_refs((nodetype, meta, body), state): + case Elixir.Enum.member?(Core.Parser.Utils.nodes_types(), nodetype): + False -> [(nodetype, meta, body), state] + True -> + [body, state] = case: + Elixir.Enumerable.impl_for(body) -> iterate_items(body, state) + True -> add_statements_refs(body, state) + True -> [body, state] + + [meta, state] = increase_node_count(meta, state) + [(nodetype, meta, body), state] + +def add_statements_refs(body, state): + [body, state] = case: + body == True -> [body, state] + body == False -> [body, state] + body == None -> [body, state] + body == [] -> [body, state] + Elixir.Kernel.is_binary(body) -> [body, state] + Elixir.Kernel.is_atom(body) -> [body, state] + Elixir.Kernel.is_number(body) -> [body, state] + Elixir.Enumerable.impl_for(body) -> iterate_items(body, state) + True -> raise "faltou esse" + + [body, state] + +def increase_node_count(meta, state <- {"node_count": node_count}): + meta = Elixir.Map.put(meta, "ref_line", node_count) + + meta_per_line_ref = case Elixir.Map.has_key?(state['meta_per_line_ref'], meta['ref_line']): + True -> + raise "node_count already present in 'meta_per_line_ref', theres a error in this pos" + False -> + Elixir.Map.put( + state['meta_per_line_ref'], node_count, Elixir.Map.delete(meta, 'node_count') + ) + + state = state + |> Elixir.Map.put("node_count", node_count + 1) + |> Elixir.Map.put("meta_per_line_ref", meta_per_line_ref) + + [meta, state] + + + +def inject_into_node_quoted_function((:statements, meta, body), function_quoted): + (:statements, meta, Elixir.Enum.concat(body, [function_quoted])) + + +def quoted_get_refs_func(meta_per_line_ref): + # return quoted verson of the folowing code: + # def __fython_get_node_ref__(key): + # refs = {1: (0, 0, 0)} + # Elixir.Map.get(regs, key) + + # TODO retirar todo esse inline quoted e fazer uma + # TODO string multilinha com o que queremos e rodar o nosso quote + # TODO muito mais fácil de endenter e mais elegante + + meta = { + "start": (0, 0, 0), + "end": (0, 0, 0), + } + + map_body_quoted = meta_per_line_ref + |> Elixir.Enum.map( + lambda (key, value): + key_quoted = (:number, meta, [key]) + (s_idx, s_ln, s_col) = value['start'] + (e_idx, e_ln, e_col) = value['end'] + + value_quoted = ( + :map, + meta, + [ + ( + (:string, meta, ["start"]), + ( + :tuple, + meta, + [ + (:number, meta, [s_idx]), + (:number, meta, [s_ln]), + (:number, meta, [s_col]), + ] + ) + ), + ( + (:string, meta, ["end"]), + ( + :tuple, + meta, + [ + (:number, meta, [e_idx]), + (:number, meta, [e_ln]), + (:number, meta, [e_col]), + ] + ) + ), + ] + ) + + (key_quoted, value_quoted) + ) + + ( + :def, + Elixir.Map.put(meta, "docstring", None), + [ + "__fython_get_node_ref__", + [ + (:var, meta, [False, "key"]) + ], + ( + :statements, + meta, + [ + ( + :pattern, + meta, + [ + (:var, meta, [False, "refs"]), + ( + :map, + meta, + map_body_quoted + ) + ] + ), + ( + :call, + meta, + [ + (:var, meta, [False, "Elixir.Map.get"]), + [ + (:var, meta, [False, "refs"]), + (:var, meta, [False, "key"]) + ], + [], + False + ] + ) + ] + ) + ] + ) + +def quoted_source_code_func(source_code): + meta = {"start": (0,0,0), "end": (0,0,0)} + + ( + :def, + Elixir.Map.put(meta, "docstring", None), + [ + "__fython_get_file_source_code__", + [], + ( + :statements, + meta, + [(:string, meta, [source_code])] + ) + ] + ) \ No newline at end of file diff --git a/src/core/parser/utils.fy b/src/core/parser/utils.fy index 5473e19..31443bb 100644 --- a/src/core/parser/utils.fy +++ b/src/core/parser/utils.fy @@ -21,3 +21,29 @@ def set_error(state, msg, pos_start, pos_end): ) state _ -> state + +def nodes_types(): + [ + :number, + :atom, + :var, + :string, + :unary, + :list, + :tuple, + :binop, + :pattern, + :if, + :func, + :statements, + :lambda, + :def, + :static_access, + :raise, + :pipe, + :map, + :case, + :call, + :try, + :range, + ] \ No newline at end of file diff --git a/src/exceptions/__init__.fy b/src/exceptions/__init__.fy new file mode 100644 index 0000000..face3a8 --- /dev/null +++ b/src/exceptions/__init__.fy @@ -0,0 +1,50 @@ +def format_traceback(error, stacktrace): + formated_lines = stacktrace + |> Elixir.Enum.filter(&is_fython_stack/1) + |> Elixir.Enum.reverse() + |> Elixir.Enum.map(&format_line_stacktrace/1) + |> Elixir.Enum.join("") + + code_with_error = source_code_with_error(Elixir.Enum.at(stacktrace, 0)) + + Elixir.IO.puts("Traceback (most recent call last):") + Elixir.IO.puts(formated_lines) + Elixir.IO.puts(code_with_error) + +def ensure_string_module_name(module): + module + |> Elixir.Atom.to_string() + |> Elixir.String.replace_prefix("Fython.", "") + +def get_module_source_code(module): + module = ensure_string_module_name(module) + code_to_run = Elixir.Enum.join([module, '.__fython_get_file_source_code__()']) + (result, _) = Core.eval_string(code_to_run) + result + +def line_meta_of_line_of_module(module, line_ref): + module = ensure_string_module_name(module) + code_to_run = Elixir.Enum.join([module, '.__fython_get_node_ref__(', line_ref, ')']) + (result, _) = Core.eval_string(code_to_run) + +def format_line_stacktrace((module, func_name, _, [(:file, file), (:line, line)])): + format_line_stacktrace((module, func_name, None, [(:file, file), (:line, line), (:error_info, None)])) + +def format_line_stacktrace((module, func_name, _, [(:file, file), (:line, line), (:error_info, _error_info)])): + module = ensure_string_module_name(module) + + Elixir.Enum.join([" file: ", file, ", line: ", line, "\n", " ", module, ".", func_name, "\n"], "") + +def source_code_with_error((module, func_name, _, [(:file, file), (:line, line)])): + source_code_with_error((module, func_name, None, [(:file, file), (:line, line), (:error_info, None)])) + +def source_code_with_error((module, func_name, _, [(:file, file), (:line, line), (:error_info, _error_info)])): + source_code = get_module_source_code(module) + + Elixir.IO.inspect(line_meta_of_line_of_module(module, line)) + + source_code + +def is_fython_stack(stack): + module = stack |> Elixir.Kernel.elem(0) + Elixir.Atom.to_string(module) |> Elixir.String.starts_with?("Fython.") diff --git a/tests/math_tests.fy b/tests/math_tests.fy index 39a20ab..91cfb33 100644 --- a/tests/math_tests.fy +++ b/tests/math_tests.fy @@ -1,4 +1,5 @@ def run_tests(): + Elixir.IO.puts("Running tests") test_numbers() test_basic_operations() test_strings() @@ -9,6 +10,8 @@ def run_tests(): test_pattern_matching() test_pipe_operator() test_case() + test_function_call() + Elixir.IO.puts("All passed") def test_numbers(): assert_equal(1_000, 1000) @@ -32,6 +35,7 @@ def test_basic_operations(): def test_strings(): assert_equal('a', "a") + assert_equal('"', "\"") def test_lists(): a = [1, 2, 3] @@ -80,7 +84,7 @@ def test_pipe_operator(): def test_range(): assert_equal(Elixir.Enum.to_list(1..5), [1, 2, 3, 4, 5]) - assert_equal(Elixir.Enum.to_list(5..0), [5, 4, 3, 2, 1]) + assert_equal(Elixir.Enum.to_list(5..1), [5, 4, 3, 2, 1]) def test_case(): a = case True: @@ -98,7 +102,22 @@ def test_case(): _ -> False assert_equal(a, True) +def test_function_call(): + assert_equal(sum_function(10, 15), 25) + assert_equal(sum_function([10, 15]), 25) + assert_equal(sum_function({"a": 10, "b": 15}), 25) + +def sum_function(a, b): + a + b + +def sum_function([a, b]): + a + b + +def sum_function({"a": a, "b": b}): + a + b + def assert_equal(a, b): case a == b: False -> raise "It's not equal" - True -> None \ No newline at end of file + True -> None +