diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cbf7b8b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +**/_compiled/ +/bin/ +/Makefile +**/erl_crash.dump \ No newline at end of file diff --git a/Makefile b/Makefile index 2907110..167d844 100644 --- a/Makefile +++ b/Makefile @@ -1,55 +1,31 @@ SHELL := /bin/bash -ev=latest - -.SILENT: compile, bootstrap - -TEMP_FILES = /tmp -THIS_GIT_VERSION := $(shell git tag | tail -1) - -INSTALATION_PATH := /opt/fython # makefile location ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) # fython src folder SRC_DIR:="$(ROOT_DIR)/src" -install-from-source: - $(MAKE) compile-source DESTINE_PATH=$(INSTALATION_PATH) - -shell: - erl -pa $(INSTALATION_PATH) -s 'Fython.Shell' start -noshell - -compile-source: - echo "Bootstraping with version: " $(THIS_GIT_VERSION) - echo "Destine path: " $(DESTINE_PATH) - - $(eval OUTPUT_ZIP_PATH := $(TEMP_FILES)/fython_$(THIS_GIT_VERSION)_compiled.tar.gz) - wget https://github.com/joaovitorsilvestre/fython/releases/download/$(THIS_GIT_VERSION)/_compiled.tar.gz -O $(OUTPUT_ZIP_PATH) - - $(eval COMPILED_OUTPUT_PATH := $(TEMP_FILES)/fython_$(THIS_GIT_VERSION)_compiled_local) - sudo mkdir -p $(COMPILED_OUTPUT_PATH) 2>/dev/null - sudo tar -xf $(OUTPUT_ZIP_PATH) -C $(COMPILED_OUTPUT_PATH) - - $(MAKE) bootstrap PRE_COMPILER=$(COMPILED_OUTPUT_PATH) DESTINE_PATH=$(DESTINE_PATH) - -bootstrap: - # e.g: make bootstrap PRE_COMPILER=/home/joao/fython/src/_compiled DESTINE_PATH=/home/joao/fython/src/_bootstrap - - $(eval ELIXIR_PATH = "/usr/lib/elixir/lib/elixir/ebin") - $(eval ALL_FILES_PATH := $(shell find $(SRC_DIR) -name '*.fy')) - - echo "Bootstraping" - echo "pre compiler: "$(PRE_COMPILER) - echo "destine folder: "$(DESTINE_PATH) - - sudo rm -rf $(DESTINE_PATH) || 0 - sudo mkdir $(DESTINE_PATH) - - # copy elixir modules - sudo cp -r $(ELIXIR_PATH)/* $(DESTINE_PATH) - - # compile all the modules idependitly - for FILE_PATH in $(ALL_FILES_PATH); do ./functions.sh exec_in_erl ${SRC_DIR} $${FILE_PATH} ${DESTINE_PATH} ${PRE_COMPILER}; done - -compress-to-release: - cd $(FOLDER_PATH)/ && tar -zcvf $(ROOT_DIR)/_compiled.tar.gz * && cd - +bootstrap-with-docker: + @DOCKER_TAG="fython:bootstrap" + docker build -f devtools/Dockerfile -t $$DOCKER_TAG --target base . + +.ONESHELL: +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 + +.ONESHELL: +run-tests: + @DOCKER_TAG="fython:tests" + docker build -f devtools/Dockerfile -t $$DOCKER_TAG --target tests . + docker run $$DOCKER_TAG + + +.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 diff --git a/README.md b/README.md index fcb2fcf..d494bb8 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ b = 'RPAREN' case b: a -> False # this is not working ``` -- [ ] Some times the error arrow is showing in wrong place. Eg: +- [ ] Sometimes the error arrow is showing in wrong place. Eg: ``` def add(a) a + b @@ -181,6 +181,15 @@ a = lambda: lambda : "" a()() ``` +- [ ] Support of 'pass' keyword +- [ ] Create function to use for operator + + - this will allow us to create multiple functions depending on params + - if its two strings, concatenate, if is lists, merge them, etc + - today elixir has diferent functions, to merge lists is ++ and strings <> +- [ ] Improve way to access tuples elements, today we need to use Elixir.Kernel.elem + - Would be great if our Enum module worked with tuples too! +- [ ] Create simple lib of testing that collect functions in folder and run them +- [ ] Print function - [ ] Support to struct - [ ] PosParser -> Add logic to check imports, undefined vars, etc. - [x] PosParser -> support for the pin variable in pattern matching: `e = "a""; {^e: 1} = {"a": 1}` diff --git a/devtools/Dockerfile b/devtools/Dockerfile new file mode 100644 index 0000000..b8f232b --- /dev/null +++ b/devtools/Dockerfile @@ -0,0 +1,51 @@ +FROM elixir:1.14.1 as base + +# Arguments you can modify +ARG DESTINE_PATH="/compiled" +ARG VERSION_TO_USE_AS_BOOTSTRAPER="v0.5.2" + +ENV DESTINE_PATH=$DESTINE_PATH +ENV PATH_FYTHON_TO_USE_AS_BOOTSTRAPER="/fython_$VERSION_TO_USE_AS_BOOTSTRAPER" +ENV ELIXIR_BEAMS_PATH="/usr/local/lib/elixir/lib/elixir/ebin/" +ENV IEX_BEAMS_PATH="/usr/local/lib/elixir/lib/iex/ebin/" + +# geting older version to use for bootstrap +ENV OUTPUT_ZIP_PATH="/fython_$VERSION_TO_USE_AS_BOOTSTRAPER_compiled.tar.gz" +RUN wget https://github.com/joaovitorsilvestre/fython/releases/download/$VERSION_TO_USE_AS_BOOTSTRAPER/_compiled.tar.gz -O $OUTPUT_ZIP_PATH \ + && mkdir -p $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER \ + && tar -xf $OUTPUT_ZIP_PATH -C $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER + +COPY src src +COPY /devtools/bootstrap.sh /bootstrap.sh +COPY /devtools/fython /fython +RUN chmod a+x /bootstrap.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 + +# Remove base used for bootstrap (to ensure that we are not using it anymore) +RUN rm -rf $PATH_FYTHON_TO_USE_AS_BOOTSTRAPER + +# 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 + +FROM base as elixir_shell +# TODO +# CMD erl -pa $IEX_BEAMS_PATH -noshell -user Elixir.IEx.CLI +iex + +FROM base as shell +CMD /fython exec "Shell.start()" $DESTINE_PATH + +FROM base as compiler +ENV PROJET_FOLDER="/project" +CMD echo "Compiling project: $PROJET_FOLDER" \ + && /fython exec "Core.Code.compile_project('$PROJET_FOLDER')" $DESTINE_PATH + +FROM base as tests +COPY tests tests + +CMD /fython exec "Core.Code.compile_project('/tests')" $DESTINE_PATH \ + && /fython exec "Math_tests.run_tests()" $DESTINE_PATH /tests/_compiled diff --git a/devtools/bootstrap.sh b/devtools/bootstrap.sh new file mode 100755 index 0000000..1183b7c --- /dev/null +++ b/devtools/bootstrap.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e # 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 + + 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 / + + 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"; + done +} + +$* diff --git a/devtools/fython b/devtools/fython new file mode 100644 index 0000000..aca6925 --- /dev/null +++ b/devtools/fython @@ -0,0 +1,15 @@ +#!/bin/bash +set -e # stop if any error + +exec () { + FYTHON_CODE=$1 + ERLANG_PATHS=${@:2} + + ERLANG_PATHS_FORMATED="" + 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 +} + +$* \ No newline at end of file diff --git a/functions.sh b/functions.sh deleted file mode 100755 index f7f0ab1..0000000 --- a/functions.sh +++ /dev/null @@ -1,16 +0,0 @@ -exec_in_erl () { - SRC_DIR=$1 - FILE_PATH=$2 - BOOTSTRAP_FOLDER=$3 - PRE_COMPILER=$4 - 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}'"'">>, <<"'"'${FILE_PATH}'"'">>, "'"'${BOOTSTRAP_FOLDER}'"'"), init:stop()." - sudo erl -pa $PRE_COMPILER -noshell -eval "$ERL_COMMAND_CALL" -# echo "SRC_DIR: " $SRC_DIR -# echo "FILE_PATH: " $FILE_PATH -# echo "BOOTSTRAP_FOLDER: " $BOOTSTRAP_FOLDER -# echo "PRE_COMPILER: " $PRE_COMPILER -# echo '-----' -} - -# Allows to call a function based on arguments passed to the script -$* \ No newline at end of file diff --git a/src/bootstrap.sh b/src/bootstrap.sh deleted file mode 100644 index 9df70bd..0000000 --- a/src/bootstrap.sh +++ /dev/null @@ -1,31 +0,0 @@ -CURRENT_DIR=$(pwd) - -ALL_FILES_PATH=$(find "$(pwd)" -name '*.fy') - -PRE_COMPILER=${1:-"./_compiled"} -BOOTSTRAP_FOLDER=${2:-"_bootstrap"} - -ELIXIR_PATH='/usr/lib/elixir/lib/elixir/ebin' - -echo "Bootstraping" -echo "pre compiler: "$CURRENT_DIR/$PRE_COMPILER -echo "destine folder: "$CURRENT_DIR/$BOOTSTRAP_FOLDER -echo "" - -rm -rf $BOOTSTRAP_FOLDER || 0 -mkdir $BOOTSTRAP_FOLDER - -# copy elixir modules -cp -r $ELIXIR_PATH/* $CURRENT_DIR/$BOOTSTRAP_FOLDER - -exec_in_erl () { - FILE_PATH=$1 - ERL_COMMAND_CALL="application:start(compiler), application:start(elixir), 'Elixir.Code':compiler_options(#{ignore_module_conflict => true}), 'Fython.Core.Code':compile_project_file(<<"'"'${CURRENT_DIR}'"'">>, <<"'"'${FILE_PATH}'"'">>, "'"'${BOOTSTRAP_FOLDER}'"'"), init:stop()." - erl -pa $PRE_COMPILER -noshell -eval "$ERL_COMMAND_CALL" -} - -# compile all the modules idependitly -for FILE_PATH in $ALL_FILES_PATH; do exec_in_erl $FILE_PATH & done -wait - -echo "Finished" \ No newline at end of file diff --git a/src/core/code.fy b/src/core/code.fy index 08385da..4861e1a 100644 --- a/src/core/code.fy +++ b/src/core/code.fy @@ -18,7 +18,7 @@ def compile_project(project_path, destine): |> Elixir.Enum.join('/') |> Elixir.Path.wildcard() |> Elixir.Enum.map(lambda file_full_path: - compile_project_file(project_path, file_full_path, destine) + compile_project_file(project_path, file_full_path, compiled_folder) ) @@ -40,8 +40,6 @@ def copy_elixir_beams(compiled_folder): def compile_project_file(project_root, file_full_path, destine_compiled): module_name = get_module_name(project_root, file_full_path) - destine_compiled = Elixir.Enum.join([destine_compiled]) - # Ensure compiled folder is created Elixir.File.mkdir_p!(destine_compiled) diff --git a/src/core/lexer/__init__.fy b/src/core/lexer/__init__.fy index ce74185..8f07963 100644 --- a/src/core/lexer/__init__.fy +++ b/src/core/lexer/__init__.fy @@ -236,6 +236,7 @@ def loop_while(st, func): case valid: True -> Elixir.Map.put(st, "result", Elixir.Enum.join([result, cc])) |> loop_while(func) False -> st + _ -> set_error(st, valid) def loop_until_sequence(state, expected_seq): @@ -362,9 +363,15 @@ def make_number(state): None -> False _ -> Elixir.String.contains?(Elixir.Enum.join([Core.Lexer.Consts.digists(), '._']), cc) - valid_num_char and cc != "." and nc != "." + case (valid_num_char, cc, nc): + (False, _, _) -> False + (True, ".", ".") -> False # Range + (True, _, _) -> True + _ -> False ) + result = Elixir.Enum.join([first_number, Elixir.Map.get(state, "result")]) + result = Elixir.String.replace(result, "_", "") state = case: Elixir.String.contains?(result, '.') -> diff --git a/tests/math_tests.fy b/tests/math_tests.fy new file mode 100644 index 0000000..39a20ab --- /dev/null +++ b/tests/math_tests.fy @@ -0,0 +1,104 @@ +def run_tests(): + test_numbers() + test_basic_operations() + test_strings() + test_dicts() + test_lists() + test_lambda() + test_tuples() + test_pattern_matching() + test_pipe_operator() + test_case() + +def test_numbers(): + assert_equal(1_000, 1000) + assert_equal(1_000_000, 1000000) + assert_equal(1_000.5, 1000 + 0.5) + +def test_basic_operations(): + assert_equal(1 / 2, 0.5) + assert_equal(1 + 2 - 3, 0) + assert_equal(1 - 2 - 3, -4) + assert_equal(1 + 2, 3) + assert_equal(90 / 2, 45) + assert_equal(90 / 2 + 10, 55) + assert_equal(10 + 10 / 10, 11) + assert_equal((10 + 10) / 10, 2) + assert_equal(10 * 10 / 20, 5) + assert_equal(10 * (10 / 20), 5) + assert_equal(3 ** 2, 9) + assert_equal(3 ** 3, 27) + assert_equal(3 ** 3 * 2, 54) + +def test_strings(): + assert_equal('a', "a") + +def test_lists(): + a = [1, 2, 3] + assert_equal(Elixir.Enum.at(a, 0), 1) + assert_equal(Elixir.Enum.at(a, 1), 2) + assert_equal(Elixir.Enum.at(a, 2), 3) + +def test_dicts(): + a = {'a': 2} + assert_equal(a['a'], 2) + + a = Elixir.Map.put(a, 'b', 20) + assert_equal(a['b'], 20) + assert_equal(a, {"a": 2, 'b': 20}) + + key = 50 + a = {key: "a"} + assert_equal(a[50], "a") + +def test_lambda(): + a = lambda a, b: a + b + assert_equal(a(10, 15), 25) + + assert_equal(Elixir.Enum.map([1, 2, 3], lambda x: x + 1), [2, 3, 4]) + +def test_tuples(): + assert_equal((10,) |> Elixir.Kernel.elem(0), 10) + assert_equal((1, 2) |> Elixir.Kernel.elem(0), 1) + assert_equal((1, 2) |> Elixir.Kernel.elem(1), 2) + +def test_pattern_matching(): + {"a": a} = {"a": 2} + assert_equal(a, 2) + + (a,) = (1,) + assert_equal(a, 1) + + [a] = [10] + assert_equal(a, 10) + + {"nested": {"ola": ([a], 10)}} = {"nested": {"ola": ([50], 10)}} + assert_equal(a, 50) + +def test_pipe_operator(): + [1, 2, 3] |> Elixir.Enum.map(lambda x: x * -1) |> assert_equal([-1, -2, -3]) + +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]) + +def test_case(): + a = case True: + True -> "s" + False -> "n" + assert_equal(a, "s") + + a = case {"a": 20}: + {"a": 10} -> True + _ -> False + assert_equal(a, False) + + a = case {"a": 20}: + {"a": 20} -> True + _ -> False + assert_equal(a, True) + +def assert_equal(a, b): + case a == b: + False -> raise "It's not equal" + True -> None \ No newline at end of file