diff --git a/.github/workflows/build-docs.yaml b/.github/workflows/build-docs.yaml index e2c20908e..ecaf3e5ff 100644 --- a/.github/workflows/build-docs.yaml +++ b/.github/workflows/build-docs.yaml @@ -91,6 +91,14 @@ jobs: repository: ${{ vars.GITHUB_REPOSITORY }} fetch-depth: 0 + - name: Track all branches + shell: bash + run: | + git config --global --add safe.directory /__w/AtomVM/AtomVM + for branch in `git branch -a | grep "remotes/origin" | grep -v HEAD | grep -v "${{ github.ref_name }}"`; do + git branch --track ${branch#remotes/origin/} $branch + done + - name: Build Site shell: bash run: | diff --git a/.github/workflows/esp32-build.yaml b/.github/workflows/esp32-build.yaml index d6ad572ac..ab8a6971b 100644 --- a/.github/workflows/esp32-build.yaml +++ b/.github/workflows/esp32-build.yaml @@ -13,12 +13,14 @@ on: - 'CMakeLists.txt' - 'libs/**' - 'src/platforms/esp32/**' + - 'src/platforms/esp32/**/**' - 'src/libAtomVM/**' - 'tools/packbeam/**' pull_request: paths: - '.github/workflows/esp32-build.yaml' - 'src/platforms/esp32/**' + - 'src/platforms/esp32/**/**' - 'src/libAtomVM/**' concurrency: @@ -36,17 +38,14 @@ jobs: matrix: esp-idf-target: ["esp32", "esp32c3"] idf-version: - - 'v4.4.7' - - 'v5.0.6' + - 'v5.0.7' - 'v5.1.4' - 'v5.2.2' - - 'v5.3-rc1' + - 'v5.3.1' exclude: - esp-idf-target: "esp32c3" - idf-version: 'v4.4.7' - - esp-idf-target: "esp32c3" - idf-version: 'v5.0.6' + idf-version: 'v5.0.7' - esp-idf-target: "esp32c3" idf-version: 'v5.1.4' steps: diff --git a/.github/workflows/esp32-mkimage.yaml b/.github/workflows/esp32-mkimage.yaml index e938ab58c..ff59d0e2f 100644 --- a/.github/workflows/esp32-mkimage.yaml +++ b/.github/workflows/esp32-mkimage.yaml @@ -13,12 +13,14 @@ on: - 'CMakeLists.txt' - 'libs/**' - 'src/platforms/esp32/**' + - 'src/platforms/esp32/**/**' - 'src/libAtomVM/**' - 'tools/packbeam/**' pull_request: paths: - '.github/workflows/esp32-mkimage.yaml' - 'src/platforms/esp32/**' + - 'src/platforms/esp32/**/**' - 'src/libAtomVM/**' permissions: @@ -30,26 +32,27 @@ concurrency: jobs: esp32-release: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 container: espressif/idf:v${{ matrix.idf-version }} strategy: matrix: - idf-version: ["5.1.4"] - cc: ["clang-10"] - cxx: ["clang++-10"] + idf-version: ["5.3.1"] + cc: ["clang-14"] + cxx: ["clang++-14"] cflags: ["-O3"] otp: ["27"] elixir_version: ["1.17"] - compiler_pkgs: ["clang-10"] + compiler_pkgs: ["clang-14"] soc: ["esp32", "esp32c2", "esp32c3", "esp32s2", "esp32s3", "esp32c6", "esp32h2"] + flavor: ["", "-elixir"] env: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} CFLAGS: ${{ matrix.cflags }} CXXFLAGS: ${{ matrix.cflags }} - ImageOS: "ubuntu20" + ImageOS: "ubuntu22" steps: - name: Checkout repo @@ -117,27 +120,39 @@ jobs: run: | cp sdkconfig.release-defaults sdkconfig.defaults - - name: "Build ${{ matrix.soc }} with idf.py" + - name: "Build ${{ matrix.soc }}${{ matrix.flavor }} with idf.py" shell: bash working-directory: ./src/platforms/esp32/ run: | rm -rf build . $IDF_PATH/export.sh + if [ ! -z "${{ matrix.flavor }}" ] + then + mv partitions${{ matrix.flavor }}.csv partitions.csv + fi idf.py set-target ${{ matrix.soc }} idf.py reconfigure idf.py build - - name: "Create a ${{ matrix.soc }} image" + - name: "Create a ${{ matrix.soc }}${{ matrix.flavor }} image" working-directory: ./src/platforms/esp32/build run: | - ./mkimage.sh + if [ -z "${{ matrix.flavor }}" ] + then + ./mkimage.sh + else + FLAVOR_SUFFIX=$(echo "${{ matrix.flavor }}" | sed 's/-//g') + BOOT_FILE="build/libs/esp32boot/${FLAVOR_SUFFIX}_esp32boot.avm" + ./mkimage.sh --boot="$BOOT_FILE" + mv atomvm-${{ matrix.soc }}.img atomvm-${{ matrix.soc }}${{ matrix.flavor }}.img + fi ls -l *.img - name: "Upload ${{ matrix.soc }} artifacts" uses: actions/upload-artifact@v4 with: - name: atomvm-${{ matrix.soc }}-image - path: ./src/platforms/esp32/build/atomvm-${{ matrix.soc }}.img + name: atomvm-${{ matrix.soc }}${{ matrix.flavor }}-image + path: ./src/platforms/esp32/build/atomvm-${{ matrix.soc }}${{ matrix.flavor }}.img if-no-files-found: error - name: "Rename and write sha256sum" @@ -145,8 +160,8 @@ jobs: shell: bash working-directory: src/platforms/esp32/build run: | - ATOMVM_IMG="AtomVM-${{ matrix.soc }}-${{ github.ref_name }}.img" - mv atomvm-${{ matrix.soc }}.img "${ATOMVM_IMG}" + ATOMVM_IMG="AtomVM-${{ matrix.soc }}${{ matrix.flavor }}-${{ github.ref_name }}.img" + mv atomvm-${{ matrix.soc }}${{ matrix.flavor }}.img "${ATOMVM_IMG}" sha256sum "${ATOMVM_IMG}" > "${ATOMVM_IMG}.sha256" - name: Release @@ -156,5 +171,5 @@ jobs: draft: true fail_on_unmatched_files: true files: | - src/platforms/esp32/build/AtomVM-${{ matrix.soc }}-${{ github.ref_name }}.img - src/platforms/esp32/build/AtomVM-${{ matrix.soc }}-${{ github.ref_name }}.img.sha256 + src/platforms/esp32/build/AtomVM-${{ matrix.soc }}${{ matrix.flavor }}-${{ github.ref_name }}.img + src/platforms/esp32/build/AtomVM-${{ matrix.soc }}${{ matrix.flavor }}-${{ github.ref_name }}.img.sha256 diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml index ff8e52f9e..cde1c5242 100644 --- a/.github/workflows/publish-docs.yaml +++ b/.github/workflows/publish-docs.yaml @@ -98,6 +98,14 @@ jobs: ref: Production path: /home/runner/work/AtomVM/AtomVM/www + - name: Track all branches + shell: bash + run: | + git config --global --add safe.directory /__w/AtomVM/AtomVM + for branch in `git branch -a | grep "remotes/origin" | grep -v HEAD | grep -v "${{ github.ref_name }}" `; do + git branch --track ${branch#remotes/origin/} $branch + done + - name: Build Site shell: bash run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8550bb9..fec594ddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a limited implementation of the OTP `ets` interface - Added `code:all_loaded/0` and `code:all_available/0` +## [0.6.5] - Unreleased + +### Added + +- ESP32: add a new Elixir release "flavor" with a bigger boot.avm partition that has room for +Elixir standard library modules +- ESP32: `--boot` option to mkimage.sh tool +- Add `erlang:atom_to_binary/1` that is equivalent to `erlang:atom_to_binary(Atom, utf8)` +- Support for Elixir `String.Chars` protocol, now functions such as `Enum.join` are able to take +also non string parameters (e.g. `Enum.join([1, 2], ",")` +- Support for Elixir `Enum.at/3` +- Add support for `is_bitstring/1` construct which is used in Elixir protocols runtime. +- Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each`, +`Enum.filter`, `Enum.flat_map`, `Enum.reject`, `Enum.chunk_by` and `Enum.chunk_while` +- Support for `maps:merge_with/3` +- Support for `lists:last/1` and `lists:mapfoldl/3` +- Add support to Elixir for `Process.send/2` `Process.send_after/3/4` and `Process.cancel_timer/1` +- Add support for `handle_continue` callback in `gen_server` +- Support for Elixir `List.Chars` protocol +- Support for `gen_server:start_monitor/3,4` + +### Changed + +- ESP32: Elixir library is not shipped anymore with `esp32boot.avm`. Use `elixir_esp32boot.avm` +instead +- `Enum.find_index` and `Enum.find_value` support Enumerable and not just lists + +### Fixed + +- ESP32: content of `boot.avm` partition is not truncated anymore +- ESP32: `Fixed gpio:set_int` to accept any pin, not only pin 2 +- Fix memory corruption in `unicode:characters_to_binary` +- Fix handling of large literal indexes +- `unicode:characters_to_list`: fixed bogus out_of_memory error on some platforms such as ESP32 +- Fix crash in Elixir library when doing `inspect(:atom)` +- General inspect() compliance with Elixir behavior (but there are still some minor differences) + ## [0.6.4] - 2024-08-18 ### Added diff --git a/UPDATING.md b/UPDATING.md index c9a941c11..d914fe423 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -6,6 +6,14 @@ # AtomVM Update Instructions +## v0.6.4 -> v0.6.5 + +- ESP32: `esp32boot.avm` doesn't contain anymore Elixir standard library, use instead +`elixir_esp32boot.avm`, or the Elixir release flavor when using an image. +- ESP32: partitioning schema for Elixir flavor is different, so app offset has been changed for +Elixir images. Make sure to use `0x250000` as offset in your mix.exs or when performing manual +flashing. + ## v0.6.0-beta.1 -> v0.6.0-rc.0 - Drivers that send messages from Esp32 callbacks should use new functions diff --git a/doc/conf.py.in b/doc/conf.py.in index d6cdce215..52c6550a5 100644 --- a/doc/conf.py.in +++ b/doc/conf.py.in @@ -171,7 +171,7 @@ for tag in tag_list: versions.append(tag.name) release_list.append(tag.name) -omit_branch_list = ('release-0.5') +omit_branch_list = [ 'release-0.5' ] branch_list = sorted(repo.branches, key=lambda t: t.commit.committed_datetime) for branch in branch_list: if branch.name not in omit_branch_list: diff --git a/doc/release-notes.md.in b/doc/release-notes.md.in index 7d3dce993..f3f46540d 100644 --- a/doc/release-notes.md.in +++ b/doc/release-notes.md.in @@ -67,10 +67,10 @@ AtomVM currently supports the following versions of ESP-IDF: | IDF SDK supported versions | AtomVM support | |------------------------------|----------------| -| ESP-IDF [v4.4](https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32/get-started/index.html) | ✅ | -| ESP-IDF [v5.0](https://docs.espressif.com/projects/esp-idf/en/v5.0.6/esp32/get-started/index.html) | ✅ | +| ESP-IDF [v5.0](https://docs.espressif.com/projects/esp-idf/en/v5.0.7/esp32/get-started/index.html) | ✅ | | ESP-IDF [v5.1](https://docs.espressif.com/projects/esp-idf/en/v5.1.4/esp32/get-started/index.html) | ✅ | | ESP-IDF [v5.2](https://docs.espressif.com/projects/esp-idf/en/v5.2.2/esp32/get-started/index.html) | ✅ | +| ESP-IDF [v5.3](https://docs.espressif.com/projects/esp-idf/en/v5.3/esp32/get-started/index.html) | ✅ | Building the AtomVM virtual machine for ESP32 is optional. In most cases, you can simply download a release image from the AtomVM [release](https://github.com/atomvm/AtomVM/releases) repository. If you wish to work on development of the VM or use one on the additional drivers that are available in the [AtomVM repositories](https://github.com/atomvm) you will to build AtomVM from source. See the [Build Instructions](build-instructions.md) for information about how to build AtomVM from source code. We recommend you to use the latest subminor (patch) versions for source builds. You can check the current version used for testing in the [esp32-build.yaml](https://github.com/atomvm/AtomVM/actions/workflows/esp32-build.yaml) workflow. diff --git a/libs/esp32boot/CMakeLists.txt b/libs/esp32boot/CMakeLists.txt index 571034859..23cdc8795 100644 --- a/libs/esp32boot/CMakeLists.txt +++ b/libs/esp32boot/CMakeLists.txt @@ -23,7 +23,7 @@ project(esp32boot) include(BuildErlang) if (Elixir_FOUND) - pack_runnable(esp32boot esp32init esp32devmode eavmlib estdlib alisp exavmlib) -else() - pack_runnable(esp32boot esp32init esp32devmode eavmlib estdlib alisp) + pack_runnable(elixir_esp32boot esp32init esp32devmode eavmlib estdlib alisp exavmlib) endif() + +pack_runnable(esp32boot esp32init esp32devmode eavmlib estdlib alisp) diff --git a/libs/estdlib/src/gen_server.erl b/libs/estdlib/src/gen_server.erl index 022f86154..067eab8bc 100644 --- a/libs/estdlib/src/gen_server.erl +++ b/libs/estdlib/src/gen_server.erl @@ -43,6 +43,7 @@ -export([ start/3, start/4, start_link/3, start_link/4, + start_monitor/3, start_monitor/4, stop/1, stop/3, call/2, call/3, cast/2, @@ -68,29 +69,36 @@ -type init_result(StateType) :: {ok, State :: StateType} - | {ok, State :: StateType, timeout()} + | {ok, State :: StateType, timeout() | {continue, term()}} | {stop, Reason :: any()}. +-type handle_continue_result(StateType) :: + {noreply, NewState :: StateType} + | {noreply, NewState :: StateType, timeout() | {continue, term()}} + | {stop, Reason :: term(), NewState :: StateType}. + -type handle_call_result(StateType) :: {reply, Reply :: any(), NewState :: StateType} - | {reply, Reply :: any(), NewState :: StateType, timeout()} + | {reply, Reply :: any(), NewState :: StateType, timeout() | {continue, term()}} | {noreply, NewState :: StateType} - | {noreply, NewState :: StateType, timeout()} + | {noreply, NewState :: StateType, timeout() | {continue, term()}} | {stop, Reason :: any(), Reply :: any(), NewState :: StateType} | {stop, Reason :: any(), NewState :: StateType}. -type handle_cast_result(StateType) :: {noreply, NewState :: StateType} - | {noreply, NewState :: StateType, timeout()} + | {noreply, NewState :: StateType, timeout() | {continue, term()}} | {stop, Reason :: any(), NewState :: StateType}. -type handle_info(StateType) :: {noreply, NewState :: StateType} - | {noreply, NewState :: StateType, timeout()} + | {noreply, NewState :: StateType, timeout() | {continue, term()}} | {stop, Reason :: any(), NewState :: StateType}. -callback init(Args :: any()) -> init_result(any()). +-callback handle_continue(Continue :: term(), State :: StateType) -> + handle_continue_result(StateType). -callback handle_call(Request :: any(), From :: {pid(), Tag :: any()}, State :: StateType) -> handle_call_result(StateType). -callback handle_cast(Request :: any(), State :: StateType) -> @@ -102,9 +110,9 @@ %% @private do_spawn(Module, Args, Options, SpawnOpts) -> - Pid = spawn_opt(?MODULE, init_it, [self(), Module, Args, Options], SpawnOpts), - case wait_ack(Pid) of - ok -> {ok, Pid}; + PidOrMonRet = spawn_opt(?MODULE, init_it, [self(), Module, Args, Options], SpawnOpts), + case wait_ack(PidOrMonRet) of + ok -> {ok, PidOrMonRet}; {error, Reason} -> {error, Reason} end. @@ -116,6 +124,15 @@ do_spawn(Name, Module, Args, Options, SpawnOpts) -> {error, Reason} -> {error, Reason} end. +%% @private +spawn_if_not_registered(Name, Module, Args, Options, SpawnOpts) -> + case erlang:whereis(Name) of + undefined -> + do_spawn(Name, Module, Args, [{name, Name} | Options], SpawnOpts); + Pid -> + {error, {already_started, Pid}} + end. + init_it(Starter, Name, Module, Args, Options) -> try erlang:register(Name, self()) of true -> @@ -154,6 +171,16 @@ init_it(Starter, Module, Args, Options) -> }, infinity }; + {ok, ModState, {continue, NewContinue}} -> + init_ack(Starter, ok), + { + #state{ + name = proplists:get_value(name, Options), + mod = Module, + mod_state = ModState + }, + {continue, NewContinue} + }; {ok, ModState, InitTimeout} -> init_ack(Starter, ok), { @@ -184,6 +211,7 @@ init_it(Starter, Module, Args, Options) -> end, case StateT of undefined -> ok; + {State, {continue, Continue}} -> loop(State, {continue, Continue}); {State, Timeout} -> loop(State, Timeout) end. @@ -191,7 +219,11 @@ init_ack(Parent, Return) -> Parent ! {ack, self(), Return}, ok. -wait_ack(Pid) -> +wait_ack(Pid) when is_pid(Pid) -> + receive + {ack, Pid, Return} -> Return + end; +wait_ack({Pid, _MonRef}) when is_pid(Pid) -> receive {ack, Pid, Return} -> Return end. @@ -228,12 +260,7 @@ crash_report(ErrStr, Parent, E, S) -> Options :: options() ) -> {ok, pid()} | {error, Reason :: term()}. start({local, Name}, Module, Args, Options) when is_atom(Name) -> - case erlang:whereis(Name) of - undefined -> - do_spawn(Name, Module, Args, [{name, Name} | Options], []); - Pid -> - {error, {already_started, Pid}} - end. + spawn_if_not_registered(Name, Module, Args, Options, []). %%----------------------------------------------------------------------------- %% @param Module the module in which the gen_server callbacks are defined @@ -274,12 +301,7 @@ start(Module, Args, Options) -> Options :: options() ) -> {ok, pid()} | {error, Reason :: term()}. start_link({local, Name}, Module, Args, Options) when is_atom(Name) -> - case erlang:whereis(Name) of - undefined -> - do_spawn(Name, Module, Args, [{name, Name} | Options], [link]); - Pid -> - {error, {already_started, Pid}} - end. + spawn_if_not_registered(Name, Module, Args, Options, [link]). %%----------------------------------------------------------------------------- %% @param Module the module in which the gen_server callbacks are defined @@ -298,6 +320,49 @@ start_link({local, Name}, Module, Args, Options) when is_atom(Name) -> start_link(Module, Args, Options) -> do_spawn(Module, Args, Options, [link]). +%%----------------------------------------------------------------------------- +%% @param Module the module in which the gen_server callbacks are defined +%% @param Args the arguments to pass to the module's init callback +%% @param Options the options used to create the gen_server +%% @returns the gen_server pid and monitor reference tuple if successful; +%% {error, Reason}, otherwise. +%% @doc Start and monitor an un-named gen_server. +%% +%% This function will start a gen_server instance. +%% +%% Note. The Options argument is currently ignored. +%% @end +%%----------------------------------------------------------------------------- +-spec start_monitor(Module :: module(), Args :: term(), Options :: options()) -> + {ok, {Pid :: pid(), MonRef :: reference()}} | {error, Reason :: term()}. +start_monitor(Module, Args, Options) -> + do_spawn(Module, Args, Options, [monitor]). + +%%----------------------------------------------------------------------------- +%% @param ServerName the name with which to register the gen_server +%% @param Module the module in which the gen_server callbacks are defined +%% @param Args the arguments to pass to the module's init callback +%% @param Options the options used to create the gen_server +%% @returns the gen_server pid and monitor reference tuple if successful; +%% {error, Reason}, otherwise. +%% @doc Start and monitor a named gen_server. +%% +%% This function will start a gen_server instance and register the +%% newly created process with the process registry. Subsequent calls +%% may use the gen_server name, in lieu of the process id. +%% +%% Note. The Options argument is currently ignored. +%% @end +%%----------------------------------------------------------------------------- +-spec start_monitor( + ServerName :: {local, Name :: atom()}, + Module :: module(), + Args :: term(), + Options :: options() +) -> {ok, {Pid :: pid(), MonRef :: reference()}} | {error, Reason :: term()}. +start_monitor({local, Name}, Module, Args, Options) when is_atom(Name) -> + spawn_if_not_registered(Name, Module, Args, Options, [monitor]). + %%----------------------------------------------------------------------------- %% @equiv stop(ServerRef, normal, infinity) %% @doc Stop a previously started gen_server instance. @@ -434,6 +499,15 @@ reply({Pid, Ref}, Reply) -> %% %% @private +loop(#state{mod = Mod, mod_state = ModState} = State, {continue, Continue}) -> + case Mod:handle_continue(Continue, ModState) of + {noreply, NewModState} -> + loop(State#state{mod_state = NewModState}, infinity); + {noreply, NewModState, {continue, NewContinue}} -> + loop(State#state{mod_state = NewModState}, {continue, NewContinue}); + {stop, Reason, NewModState} -> + do_terminate(State, Reason, NewModState) + end; loop(#state{mod = Mod, mod_state = ModState} = State, Timeout) -> receive {'$call', {_Pid, _Ref} = From, Request} -> @@ -441,11 +515,16 @@ loop(#state{mod = Mod, mod_state = ModState} = State, Timeout) -> {reply, Reply, NewModState} -> ok = reply(From, Reply), loop(State#state{mod_state = NewModState}, infinity); + {reply, Reply, NewModState, {continue, Continue}} -> + ok = reply(From, Reply), + loop(State#state{mod_state = NewModState}, {continue, Continue}); {reply, Reply, NewModState, NewTimeout} -> ok = reply(From, Reply), loop(State#state{mod_state = NewModState}, NewTimeout); {noreply, NewModState} -> loop(State#state{mod_state = NewModState}, infinity); + {noreply, NewModState, {continue, Continue}} -> + loop(State#state{mod_state = NewModState}, {continue, Continue}); {noreply, NewModState, NewTimeout} -> loop(State#state{mod_state = NewModState}, NewTimeout); {stop, Reason, Reply, NewModState} -> @@ -460,6 +539,8 @@ loop(#state{mod = Mod, mod_state = ModState} = State, Timeout) -> case Mod:handle_cast(Request, ModState) of {noreply, NewModState} -> loop(State#state{mod_state = NewModState}, infinity); + {noreply, NewModState, {continue, Continue}} -> + loop(State#state{mod_state = NewModState}, {continue, Continue}); {noreply, NewModState, NewTimeout} -> loop(State#state{mod_state = NewModState}, NewTimeout); {stop, Reason, NewModState} -> diff --git a/libs/estdlib/src/lists.erl b/libs/estdlib/src/lists.erl index fb9e8ad80..d02031f8e 100644 --- a/libs/estdlib/src/lists.erl +++ b/libs/estdlib/src/lists.erl @@ -32,6 +32,7 @@ -export([ map/2, nth/2, + last/1, member/2, delete/2, reverse/1, @@ -45,6 +46,7 @@ keytake/3, foldl/3, foldr/3, + mapfoldl/3, all/2, any/2, flatten/1, @@ -90,6 +92,16 @@ nth(1, [H | _T]) -> nth(Index, [_H | T]) when Index > 1 -> nth(Index - 1, T). +%%----------------------------------------------------------------------------- +%% @param L the proper list from which to get the last item +%% @returns the last item of the list. +%% @doc Get the last item of a list. +%% @end +%%----------------------------------------------------------------------------- +-spec last(L :: nonempty_list(E)) -> E. +last([E]) -> E; +last([_H | T]) -> last(T). + %%----------------------------------------------------------------------------- %% @param E the member to search for %% @param L the list from which to get the value @@ -372,6 +384,24 @@ foldl(Fun, Acc0, [H | T]) -> Acc1 = Fun(H, Acc0), foldl(Fun, Acc1, T). +%%----------------------------------------------------------------------------- +%% @param Fun the function to apply +%% @param Acc0 the initial accumulator +%% @param List the list over which to fold +%% @returns the result of mapping and folding Fun over L +%% @doc Combine `map/2' and `foldl/3' in one pass. +%% @end +%%----------------------------------------------------------------------------- +-spec mapfoldl(fun((A, Acc) -> {B, Acc}), Acc, [A]) -> {[B], Acc}. +mapfoldl(Fun, Acc0, List1) -> + mapfoldl0(Fun, {[], Acc0}, List1). + +mapfoldl0(_Fun, {List1, Acc0}, []) -> + {?MODULE:reverse(List1), Acc0}; +mapfoldl0(Fun, {List1, Acc0}, [H | T]) -> + {B, Acc1} = Fun(H, Acc0), + mapfoldl0(Fun, {[B | List1], Acc1}, T). + %%----------------------------------------------------------------------------- %% @equiv foldl(Fun, Acc0, reverse(List)) %% @doc Fold over a list of terms, from right to left, applying Fun(E, Accum) diff --git a/libs/estdlib/src/maps.erl b/libs/estdlib/src/maps.erl index 49e80a835..2b50d5d52 100644 --- a/libs/estdlib/src/maps.erl +++ b/libs/estdlib/src/maps.erl @@ -55,6 +55,7 @@ from_keys/2, map/2, merge/2, + merge_with/3, remove/2, update/3 ]). @@ -439,6 +440,28 @@ merge(Map1, _Map2) when not is_map(Map1) -> merge(_Map1, Map2) when not is_map(Map2) -> error({badmap, Map2}). +%%----------------------------------------------------------------------------- +%% @param Combiner a function to merge values from Map1 and Map2 if a key exists in both maps +%% @param Map1 a map +%% @param Map2 a map +%% @returns the result of merging entries from `Map1' and `Map2'. +%% @doc Merge two maps to yield a new map. +%% +%% If `Map1' and `Map2' contain the same key, then the value from `Combiner(Key, Value1, Value2)' will be used. +%% +%% This function raises a `badmap' error if neither `Map1' nor `Map2' is a map. +%% @end +%%----------------------------------------------------------------------------- +-spec merge_with( + Combiner :: fun((Key, Value, Value) -> Value), Map1 :: #{Key => Value}, Map2 :: #{Key => Value} +) -> #{Key => Value}. +merge_with(Combiner, Map1, Map2) when is_map(Map1) andalso is_map(Map2) -> + iterate_merge_with(Combiner, maps:next(maps:iterator(Map1)), Map2); +merge_with(_Combiner, Map1, _Map2) when not is_map(Map1) -> + error({badmap, Map1}); +merge_with(_Combiner, _Map1, Map2) when not is_map(Map2) -> + error({badmap, Map2}). + %%----------------------------------------------------------------------------- %% @param Key the key to remove %% @param MapOrIterator the map or map iterator from which to remove the key @@ -545,6 +568,19 @@ iterate_map(Fun, {Key, Value, Iterator}, Accum) -> NewAccum = Accum#{Key => Fun(Key, Value)}, iterate_map(Fun, maps:next(Iterator), NewAccum). +%% @private +iterate_merge_with(_Combiner, none, Accum) -> + Accum; +iterate_merge_with(Combiner, {Key, Value1, Iterator}, Accum) -> + case Accum of + #{Key := Value2} -> + iterate_merge_with(Combiner, maps:next(Iterator), Accum#{ + Key := Combiner(Key, Value1, Value2) + }); + #{} -> + iterate_merge_with(Combiner, maps:next(Iterator), Accum#{Key => Value1}) + end. + %% @private iterate_merge(none, Accum) -> Accum; diff --git a/libs/exavmlib/lib/CMakeLists.txt b/libs/exavmlib/lib/CMakeLists.txt index 92deabdb6..8d26e4d0c 100644 --- a/libs/exavmlib/lib/CMakeLists.txt +++ b/libs/exavmlib/lib/CMakeLists.txt @@ -49,6 +49,7 @@ set(ELIXIR_MODULES Process Protocol.UndefinedError Range + System Tuple ArithmeticError @@ -74,6 +75,20 @@ set(ELIXIR_MODULES Collectable.List Collectable.Map Collectable.MapSet + + List.Chars + List.Chars.Atom + List.Chars.BitString + List.Chars.Float + List.Chars.Integer + List.Chars.List + + String.Chars + String.Chars.Atom + String.Chars.BitString + String.Chars.Float + String.Chars.Integer + String.Chars.List ) pack_archive(exavmlib ${ELIXIR_MODULES}) diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index c702db55b..a36d9f1a9 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -24,11 +24,18 @@ defmodule Enum do @compile {:autoload, false} @type t :: Enumerable.t() + @type acc :: any @type index :: integer @type element :: any + @type default :: any + require Stream.Reducers, as: R + defmacrop skip(acc) do + acc + end + defmacrop next(_, entry, acc) do quote(do: [unquote(entry) | unquote(acc)]) end @@ -53,14 +60,132 @@ defmodule Enum do Enumerable.reduce(enumerable, {:cont, acc}, fn x, acc -> {:cont, fun.(x, acc)} end) |> elem(1) end + @doc """ + Returns `true` if `fun.(element)` is truthy for all elements in `enumerable`. + + Iterates over the `enumerable` and invokes `fun` on each element. When an invocation + of `fun` returns a falsy value (`false` or `nil`) iteration stops immediately and + `false` is returned. In all other cases `true` is returned. + + ## Examples + + iex> Enum.all?([2, 4, 6], fn x -> rem(x, 2) == 0 end) + true + + iex> Enum.all?([2, 3, 4], fn x -> rem(x, 2) == 0 end) + false + + iex> Enum.all?([], fn x -> x > 0 end) + true + + If no function is given, the truthiness of each element is checked during iteration. + When an element has a falsy value (`false` or `nil`) iteration stops immediately and + `false` is returned. In all other cases `true` is returned. + + iex> Enum.all?([1, 2, 3]) + true + + iex> Enum.all?([1, nil, 3]) + false + + iex> Enum.all?([]) + true + + """ + @spec all?(t, (element -> as_boolean(term))) :: boolean + + def all?(enumerable, fun \\ fn x -> x end) + def all?(enumerable, fun) when is_list(enumerable) do all_list(enumerable, fun) end + def all?(enumerable, fun) do + Enumerable.reduce(enumerable, {:cont, true}, fn entry, _ -> + if fun.(entry), do: {:cont, true}, else: {:halt, false} + end) + |> elem(1) + end + + @doc """ + Returns `true` if `fun.(element)` is truthy for at least one element in `enumerable`. + + Iterates over the `enumerable` and invokes `fun` on each element. When an invocation + of `fun` returns a truthy value (neither `false` nor `nil`) iteration stops + immediately and `true` is returned. In all other cases `false` is returned. + + ## Examples + + iex> Enum.any?([2, 4, 6], fn x -> rem(x, 2) == 1 end) + false + + iex> Enum.any?([2, 3, 4], fn x -> rem(x, 2) == 1 end) + true + + iex> Enum.any?([], fn x -> x > 0 end) + false + + If no function is given, the truthiness of each element is checked during iteration. + When an element has a truthy value (neither `false` nor `nil`) iteration stops + immediately and `true` is returned. In all other cases `false` is returned. + + iex> Enum.any?([false, false, false]) + false + + iex> Enum.any?([false, true, false]) + true + + iex> Enum.any?([]) + false + + """ + @spec any?(t, (element -> as_boolean(term))) :: boolean + + def any?(enumerable, fun \\ fn x -> x end) + def any?(enumerable, fun) when is_list(enumerable) do any_list(enumerable, fun) end + def any?(enumerable, fun) do + Enumerable.reduce(enumerable, {:cont, false}, fn entry, _ -> + if fun.(entry), do: {:halt, true}, else: {:cont, false} + end) + |> elem(1) + end + + @doc """ + Finds the element at the given `index` (zero-based). + + Returns `default` if `index` is out of bounds. + + A negative `index` can be passed, which means the `enumerable` is + enumerated once and the `index` is counted from the end (for example, + `-1` finds the last element). + + ## Examples + + iex> Enum.at([2, 4, 6], 0) + 2 + + iex> Enum.at([2, 4, 6], 2) + 6 + + iex> Enum.at([2, 4, 6], 4) + nil + + iex> Enum.at([2, 4, 6], 4, :none) + :none + + """ + @spec at(t, index, default) :: element | default + def at(enumerable, index, default \\ nil) when is_integer(index) do + case slice_any(enumerable, index, 1) do + [value] -> value + [] -> default + end + end + @doc """ Returns the size of the enumerable. @@ -85,27 +210,289 @@ defmodule Enum do end end + @doc """ + Chunks the `enumerable` with fine grained control when every chunk is emitted. + + `chunk_fun` receives the current element and the accumulator and + must return `{:cont, chunk, acc}` to emit the given chunk and + continue with accumulator or `{:cont, acc}` to not emit any chunk + and continue with the return accumulator. + + `after_fun` is invoked when iteration is done and must also return + `{:cont, chunk, acc}` or `{:cont, acc}`. + + Returns a list of lists. + + ## Examples + + iex> chunk_fun = fn element, acc -> + ...> if rem(element, 2) == 0 do + ...> {:cont, Enum.reverse([element | acc]), []} + ...> else + ...> {:cont, [element | acc]} + ...> end + ...> end + iex> after_fun = fn + ...> [] -> {:cont, []} + ...> acc -> {:cont, Enum.reverse(acc), []} + ...> end + iex> Enum.chunk_while(1..10, [], chunk_fun, after_fun) + [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]] + + """ + @doc since: "1.5.0" + @spec chunk_while( + t, + acc, + (element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}), + (acc -> {:cont, chunk, acc} | {:cont, acc}) + ) :: Enumerable.t() + when chunk: any + def chunk_while(enumerable, acc, chunk_fun, after_fun) do + {_, {res, acc}} = + Enumerable.reduce(enumerable, {:cont, {[], acc}}, fn entry, {buffer, acc} -> + case chunk_fun.(entry, acc) do + {:cont, emit, acc} -> {:cont, {[emit | buffer], acc}} + {:cont, acc} -> {:cont, {buffer, acc}} + {:halt, acc} -> {:halt, {buffer, acc}} + end + end) + + case after_fun.(acc) do + {:cont, _acc} -> :lists.reverse(res) + {:cont, elem, _acc} -> :lists.reverse([elem | res]) + end + end + + @doc """ + Splits enumerable on every element for which `fun` returns a new + value. + + Returns a list of lists. + + ## Examples + + iex> Enum.chunk_by([1, 2, 2, 3, 4, 4, 6, 7, 7], &(rem(&1, 2) == 1)) + [[1], [2, 2], [3], [4, 4, 6], [7, 7]] + + """ + @spec chunk_by(t, (element -> any)) :: [list] + def chunk_by(enumerable, fun) do + reducers_chunk_by(&chunk_while/4, enumerable, fun) + end + + # Taken from Stream.Reducers + defp reducers_chunk_by(chunk_by, enumerable, fun) do + chunk_fun = fn + entry, nil -> + {:cont, {[entry], fun.(entry)}} + + entry, {acc, value} -> + case fun.(entry) do + ^value -> {:cont, {[entry | acc], value}} + new_value -> {:cont, :lists.reverse(acc), {[entry], new_value}} + end + end + + after_fun = fn + nil -> {:cont, :done} + {acc, _value} -> {:cont, :lists.reverse(acc), :done} + end + + chunk_by.(enumerable, nil, chunk_fun, after_fun) + end + + @doc """ + Invokes the given `fun` for each element in the `enumerable`. + + Returns `:ok`. + + ## Examples + + Enum.each(["some", "example"], fn x -> IO.puts(x) end) + "some" + "example" + #=> :ok + + """ + @spec each(t, (element -> any)) :: :ok def each(enumerable, fun) when is_list(enumerable) do :lists.foreach(fun, enumerable) :ok end + def each(enumerable, fun) do + reduce(enumerable, nil, fn entry, _ -> + fun.(entry) + nil + end) + + :ok + end + + @doc """ + Filters the `enumerable`, i.e. returns only those elements + for which `fun` returns a truthy value. + + See also `reject/2` which discards all elements where the + function returns a truthy value. + + ## Examples + + iex> Enum.filter([1, 2, 3], fn x -> rem(x, 2) == 0 end) + [2] + + Keep in mind that `filter` is not capable of filtering and + transforming an element at the same time. If you would like + to do so, consider using `flat_map/2`. For example, if you + want to convert all strings that represent an integer and + discard the invalid one in one pass: + + strings = ["1234", "abc", "12ab"] + + Enum.flat_map(strings, fn string -> + case Integer.parse(string) do + # transform to integer + {int, _rest} -> [int] + # skip the value + :error -> [] + end + end) + + """ + @spec filter(t, (element -> as_boolean(term))) :: list def filter(enumerable, fun) when is_list(enumerable) do filter_list(enumerable, fun) end + def filter(enumerable, fun) do + reduce(enumerable, [], R.filter(fun)) |> :lists.reverse() + end + + @doc """ + Returns the first element for which `fun` returns a truthy value. + If no such element is found, returns `default`. + + ## Examples + + iex> Enum.find([2, 3, 4], fn x -> rem(x, 2) == 1 end) + 3 + + iex> Enum.find([2, 4, 6], fn x -> rem(x, 2) == 1 end) + nil + iex> Enum.find([2, 4, 6], 0, fn x -> rem(x, 2) == 1 end) + 0 + + """ + @spec find(t, default, (element -> any)) :: element | default + def find(enumerable, default \\ nil, fun) + def find(enumerable, default, fun) when is_list(enumerable) do find_list(enumerable, default, fun) end + def find(enumerable, default, fun) do + Enumerable.reduce(enumerable, {:cont, default}, fn entry, default -> + if fun.(entry), do: {:halt, entry}, else: {:cont, default} + end) + |> elem(1) + end + + @doc """ + Similar to `find/3`, but returns the index (zero-based) + of the element instead of the element itself. + + ## Examples + + iex> Enum.find_index([2, 4, 6], fn x -> rem(x, 2) == 1 end) + nil + + iex> Enum.find_index([2, 3, 4], fn x -> rem(x, 2) == 1 end) + 1 + + """ + @spec find_index(t, (element -> any)) :: non_neg_integer | nil def find_index(enumerable, fun) when is_list(enumerable) do find_index_list(enumerable, 0, fun) end + def find_index(enumerable, fun) do + result = + Enumerable.reduce(enumerable, {:cont, {:not_found, 0}}, fn entry, {_, index} -> + if fun.(entry), do: {:halt, {:found, index}}, else: {:cont, {:not_found, index + 1}} + end) + + case elem(result, 1) do + {:found, index} -> index + {:not_found, _} -> nil + end + end + + @doc """ + Similar to `find/3`, but returns the value of the function + invocation instead of the element itself. + + ## Examples + + iex> Enum.find_value([2, 4, 6], fn x -> rem(x, 2) == 1 end) + nil + + iex> Enum.find_value([2, 3, 4], fn x -> rem(x, 2) == 1 end) + true + + iex> Enum.find_value([1, 2, 3], "no bools!", &is_boolean/1) + "no bools!" + + """ + @spec find_value(t, any, (element -> any)) :: any | nil + def find_value(enumerable, default \\ nil, fun) + def find_value(enumerable, default, fun) when is_list(enumerable) do find_value_list(enumerable, default, fun) end + def find_value(enumerable, default, fun) do + Enumerable.reduce(enumerable, {:cont, default}, fn entry, default -> + fun_entry = fun.(entry) + if fun_entry, do: {:halt, fun_entry}, else: {:cont, default} + end) + |> elem(1) + end + + @doc """ + Maps the given `fun` over `enumerable` and flattens the result. + + This function returns a new enumerable built by appending the result of invoking `fun` + on each element of `enumerable` together; conceptually, this is similar to a + combination of `map/2` and `concat/1`. + + ## Examples + + iex> Enum.flat_map([:a, :b, :c], fn x -> [x, x] end) + [:a, :a, :b, :b, :c, :c] + + iex> Enum.flat_map([{1, 3}, {4, 6}], fn {x, y} -> x..y end) + [1, 2, 3, 4, 5, 6] + + iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end) + [[:a], [:b], [:c]] + + """ + @spec flat_map(t, (element -> t)) :: list + def flat_map(enumerable, fun) when is_list(enumerable) do + flat_map_list(enumerable, fun) + end + + def flat_map(enumerable, fun) do + reduce(enumerable, [], fn entry, acc -> + case fun.(entry) do + list when is_list(list) -> :lists.reverse(list, acc) + other -> reduce(other, acc, &[&1 | &2]) + end + end) + |> :lists.reverse() + end + @doc """ Returns a list where each element is the result of invoking `fun` on each corresponding element of `enumerable`. @@ -208,10 +595,6 @@ defmodule Enum do end end - def reject(enumerable, fun) when is_list(enumerable) do - reject_list(enumerable, fun) - end - ## all? defp all_list([h | t], fun) do @@ -292,6 +675,19 @@ defmodule Enum do default end + ## flat_map + + defp flat_map_list([head | tail], fun) do + case fun.(head) do + list when is_list(list) -> list ++ flat_map_list(tail, fun) + other -> to_list(other) ++ flat_map_list(tail, fun) + end + end + + defp flat_map_list([], _fun) do + [] + end + @doc """ Inserts the given `enumerable` into a `collectable`. @@ -389,12 +785,12 @@ defmodule Enum do end @doc """ - Joins the given enumerable into a binary using `joiner` as a + Joins the given `enumerable` into a binary using `joiner` as a separator. If `joiner` is not passed at all, it defaults to the empty binary. - All items in the enumerable must be convertible to a binary, + All elements in the `enumerable` must be convertible to a binary, otherwise an error is raised. ## Examples @@ -409,6 +805,12 @@ defmodule Enum do @spec join(t, String.t()) :: String.t() def join(enumerable, joiner \\ "") + def join(enumerable, "") do + enumerable + |> map(&entry_to_string(&1)) + |> IO.iodata_to_binary() + end + def join(enumerable, joiner) when is_binary(joiner) do reduced = reduce(enumerable, :first, fn @@ -437,6 +839,27 @@ defmodule Enum do [] end + @doc """ + Returns a list of elements in `enumerable` excluding those for which the function `fun` returns + a truthy value. + + See also `filter/2`. + + ## Examples + + iex> Enum.reject([1, 2, 3], fn x -> rem(x, 2) == 0 end) + [1, 3] + + """ + @spec reject(t, (element -> as_boolean(term))) :: list + def reject(enumerable, fun) when is_list(enumerable) do + reject_list(enumerable, fun) + end + + def reject(enumerable, fun) do + reduce(enumerable, [], R.reject(fun)) |> :lists.reverse() + end + @doc """ Returns a list of elements in `enumerable` in reverse order. @@ -610,6 +1033,7 @@ defmodule Enum do @compile {:inline, entry_to_string: 1, reduce: 3} defp entry_to_string(entry) when is_binary(entry), do: entry + defp entry_to_string(entry), do: String.Chars.to_string(entry) ## drop diff --git a/libs/exavmlib/lib/Enumerable.MapSet.ex b/libs/exavmlib/lib/Enumerable.MapSet.ex index 4ff68716f..c8a64c200 100644 --- a/libs/exavmlib/lib/Enumerable.MapSet.ex +++ b/libs/exavmlib/lib/Enumerable.MapSet.ex @@ -34,6 +34,6 @@ defimpl Enumerable, for: MapSet do def slice(map_set) do size = MapSet.size(map_set) - {:ok, size, &MapSet.to_list/1} + {:ok, size, &Enumerable.List.slice(MapSet.to_list(map_set), &1, &2, size)} end end diff --git a/libs/exavmlib/lib/Kernel.ex b/libs/exavmlib/lib/Kernel.ex index 4d5088878..491212d87 100644 --- a/libs/exavmlib/lib/Kernel.ex +++ b/libs/exavmlib/lib/Kernel.ex @@ -42,14 +42,19 @@ defmodule Kernel do def inspect(term, opts \\ []) when is_list(opts) do case term do t when is_atom(t) -> - [?:, atom_to_string(t)] + atom_to_string(t, ":") t when is_integer(t) -> :erlang.integer_to_binary(t) t when is_list(t) -> - # TODO: escape unprintable lists - :erlang.list_to_binary(t) + if is_printable_list(t) do + str = :erlang.list_to_binary(t) + <> + else + [?[ | t |> inspect_join(?])] + |> :erlang.list_to_binary() + end t when is_pid(t) -> :erlang.pid_to_list(t) @@ -64,15 +69,19 @@ defmodule Kernel do |> :erlang.list_to_binary() t when is_binary(t) -> - # TODO: escape unprintable binaries - t + if is_printable_binary(t) do + <> + else + ["<<" | t |> :erlang.binary_to_list() |> inspect_join(">>")] + |> :erlang.list_to_binary() + end t when is_reference(t) -> :erlang.ref_to_list(t) |> :erlang.list_to_binary() t when is_float(t) -> - :erlang.float_to_binary(t) + :erlang.float_to_binary(term, [{:decimals, 17}, :compact]) t when is_map(t) -> [?%, ?{ | t |> inspect_kv() |> join(?})] @@ -88,6 +97,10 @@ defmodule Kernel do [inspect(e), last] end + defp inspect_join([h | e], last) when not is_list(e) do + [inspect(h), " | ", inspect(e), last] + end + defp inspect_join([h | t], last) do [inspect(h), ?,, ?\s | inspect_join(t, last)] end @@ -118,12 +131,56 @@ defmodule Kernel do ) end - defp atom_to_string(atom) do - # TODO: use unicode rather than plain latin1 - # handle spaces and special characters - :erlang.atom_to_binary(atom, :latin1) + defp atom_to_string(atom, prefix \\ "") do + case atom do + true -> + "true" + + false -> + "false" + + nil -> + "nil" + + any_atom -> + case :erlang.atom_to_binary(any_atom) do + <<"Elixir.", displayable::binary>> -> + displayable + + other -> + <> + end + end end + defp is_printable_list([]), do: false + + defp is_printable_list([char]) do + is_printable_ascii(char) + end + + defp is_printable_list([char | t]) do + if is_printable_ascii(char) do + is_printable_list(t) + else + false + end + end + + defp is_printable_list(_any), do: false + + defp is_printable_ascii(char) do + is_integer(char) and char >= 32 and char < 127 and char != ?' + end + + defp is_printable_binary(<<>>), do: true + + defp is_printable_binary(<>) when char >= 32 do + is_printable_binary(rest) + end + + defp is_printable_binary(_any), do: false + @doc """ Returns the biggest of the two given terms according to Erlang's term ordering. diff --git a/libs/exavmlib/lib/List.Chars.Atom.ex b/libs/exavmlib/lib/List.Chars.Atom.ex new file mode 100644 index 000000000..e187e4992 --- /dev/null +++ b/libs/exavmlib/lib/List.Chars.Atom.ex @@ -0,0 +1,26 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/list/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl List.Chars, for: Atom do + def to_charlist(nil), do: ~c"" + + def to_charlist(atom), do: Atom.to_charlist(atom) +end diff --git a/libs/exavmlib/lib/List.Chars.BitString.ex b/libs/exavmlib/lib/List.Chars.BitString.ex new file mode 100644 index 000000000..53fb74d76 --- /dev/null +++ b/libs/exavmlib/lib/List.Chars.BitString.ex @@ -0,0 +1,36 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/list/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl List.Chars, for: BitString do + @doc """ + Returns the given binary `term` converted to a charlist. + """ + def to_charlist(term) when is_binary(term) do + String.to_charlist(term) + end + + def to_charlist(term) do + raise Protocol.UndefinedError, + protocol: @protocol, + value: term, + description: "cannot convert a bitstring to a charlist" + end +end diff --git a/libs/exavmlib/lib/List.Chars.Float.ex b/libs/exavmlib/lib/List.Chars.Float.ex new file mode 100644 index 000000000..5331b88a7 --- /dev/null +++ b/libs/exavmlib/lib/List.Chars.Float.ex @@ -0,0 +1,27 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/list/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl List.Chars, for: Float do + def to_charlist(term) do + # TODO: :short option not yet supported right now, so :decimals+:compact should be replaced + :erlang.float_to_list(term, [{:decimals, 17}, :compact]) + end +end diff --git a/libs/exavmlib/lib/List.Chars.Integer.ex b/libs/exavmlib/lib/List.Chars.Integer.ex new file mode 100644 index 000000000..74f8dfa86 --- /dev/null +++ b/libs/exavmlib/lib/List.Chars.Integer.ex @@ -0,0 +1,26 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/list/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl List.Chars, for: Integer do + def to_charlist(term) do + Integer.to_charlist(term) + end +end diff --git a/libs/exavmlib/lib/List.Chars.List.ex b/libs/exavmlib/lib/List.Chars.List.ex new file mode 100644 index 000000000..bb666bbaf --- /dev/null +++ b/libs/exavmlib/lib/List.Chars.List.ex @@ -0,0 +1,25 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/list/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl List.Chars, for: List do + # Note that same inlining is used for the rewrite rule. + def to_charlist(list), do: list +end diff --git a/libs/exavmlib/lib/List.Chars.ex b/libs/exavmlib/lib/List.Chars.ex new file mode 100644 index 000000000..a2dafa7ca --- /dev/null +++ b/libs/exavmlib/lib/List.Chars.ex @@ -0,0 +1,39 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/list/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defprotocol List.Chars do + @moduledoc ~S""" + The `List.Chars` protocol is responsible for + converting a structure to a charlist (only if applicable). + + The only function that must be implemented is + `to_charlist/1` which does the conversion. + + The `to_charlist/1` function automatically imported + by `Kernel` invokes this protocol. + """ + + @doc """ + Converts `term` to a charlist. + """ + @spec to_charlist(t) :: charlist + def to_charlist(term) +end diff --git a/libs/exavmlib/lib/Process.ex b/libs/exavmlib/lib/Process.ex index feeaf1102..11af3d2f2 100644 --- a/libs/exavmlib/lib/Process.ex +++ b/libs/exavmlib/lib/Process.ex @@ -146,6 +146,20 @@ defmodule Process do receive after: (timeout -> :ok) end + @spec send(dest, msg) :: :ok | :noconnect | :nosuspend + when dest: dest(), + msg: any + defdelegate send(dest, msg), to: :erlang + + @spec send_after(pid | atom, term, non_neg_integer, [option]) :: reference + when option: {:abs, boolean} + def send_after(dest, msg, time, _opts \\ []) do + :erlang.send_after(time, dest, msg) + end + + @spec cancel_timer(reference) :: non_neg_integer | false | :ok + defdelegate cancel_timer(timer_ref), to: :erlang + @type spawn_opt :: :link | :monitor diff --git a/libs/exavmlib/lib/String.Chars.Atom.ex b/libs/exavmlib/lib/String.Chars.Atom.ex new file mode 100644 index 000000000..df11b88b6 --- /dev/null +++ b/libs/exavmlib/lib/String.Chars.Atom.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/string/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import Kernel, except: [to_string: 1] + +defimpl String.Chars, for: Atom do + def to_string(nil) do + "" + end + + def to_string(atom) do + Atom.to_string(atom) + end +end diff --git a/libs/exavmlib/lib/String.Chars.BitString.ex b/libs/exavmlib/lib/String.Chars.BitString.ex new file mode 100644 index 000000000..8a7fa3b4a --- /dev/null +++ b/libs/exavmlib/lib/String.Chars.BitString.ex @@ -0,0 +1,35 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/string/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import Kernel, except: [to_string: 1] + +defimpl String.Chars, for: BitString do + def to_string(term) when is_binary(term) do + term + end + + def to_string(term) do + raise Protocol.UndefinedError, + protocol: @protocol, + value: term, + description: "cannot convert a bitstring to a string" + end +end diff --git a/libs/exavmlib/lib/String.Chars.Float.ex b/libs/exavmlib/lib/String.Chars.Float.ex new file mode 100644 index 000000000..d4da1b8bb --- /dev/null +++ b/libs/exavmlib/lib/String.Chars.Float.ex @@ -0,0 +1,29 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/string/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import Kernel, except: [to_string: 1] + +defimpl String.Chars, for: Float do + def to_string(term) do + # TODO: :short option not yet supported right now, so :decimals+:compact should be replaced + :erlang.float_to_binary(term, [{:decimals, 17}, :compact]) + end +end diff --git a/libs/exavmlib/lib/String.Chars.Integer.ex b/libs/exavmlib/lib/String.Chars.Integer.ex new file mode 100644 index 000000000..3e7a7250a --- /dev/null +++ b/libs/exavmlib/lib/String.Chars.Integer.ex @@ -0,0 +1,28 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/string/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import Kernel, except: [to_string: 1] + +defimpl String.Chars, for: Integer do + def to_string(term) do + Integer.to_string(term) + end +end diff --git a/libs/exavmlib/lib/String.Chars.List.ex b/libs/exavmlib/lib/String.Chars.List.ex new file mode 100644 index 000000000..1132287f9 --- /dev/null +++ b/libs/exavmlib/lib/String.Chars.List.ex @@ -0,0 +1,26 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/string/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import Kernel, except: [to_string: 1] + +defimpl String.Chars, for: List do + def to_string(charlist), do: List.to_string(charlist) +end diff --git a/libs/exavmlib/lib/String.Chars.ex b/libs/exavmlib/lib/String.Chars.ex new file mode 100644 index 000000000..b3d8f3909 --- /dev/null +++ b/libs/exavmlib/lib/String.Chars.ex @@ -0,0 +1,44 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2013-2023 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/string/chars.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import Kernel, except: [to_string: 1] + +defprotocol String.Chars do + @moduledoc ~S""" + The `String.Chars` protocol is responsible for + converting a structure to a binary (only if applicable). + + The only function required to be implemented is + `to_string/1`, which does the conversion. + + The `to_string/1` function automatically imported + by `Kernel` invokes this protocol. String + interpolation also invokes `to_string/1` in its + arguments. For example, `"foo#{bar}"` is the same + as `"foo" <> to_string(bar)`. + """ + + @doc """ + Converts `term` to a string. + """ + @spec to_string(t) :: String.t() + def to_string(term) +end diff --git a/libs/exavmlib/lib/System.ex b/libs/exavmlib/lib/System.ex new file mode 100644 index 000000000..31c9ee7ad --- /dev/null +++ b/libs/exavmlib/lib/System.ex @@ -0,0 +1,51 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2024 Elixir Contributors +# https://github.com/elixir-lang/elixir/blob/v1.17/lib/elixir/lib/system.ex +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule System do + @compile {:autoload, false} + @type time_unit :: + :second + | :millisecond + | :microsecond + + @doc """ + Returns the current monotonic time in the given time unit. + + This time is monotonically increasing and starts in an unspecified + point in time. + """ + @spec monotonic_time(time_unit) :: integer + def monotonic_time(unit) do + :erlang.monotonic_time(unit) + end + + @doc """ + Returns the current system time in the given time unit. + + It is the VM view of the `os_time/0`. They may not match in + case of time warps although the VM works towards aligning + them. This time is not monotonic. + """ + @spec system_time(time_unit) :: integer + def system_time(unit) do + :erlang.system_time(unit) + end +end diff --git a/src/libAtomVM/bifs.gperf b/src/libAtomVM/bifs.gperf index bfaf6f45d..d441da07a 100644 --- a/src/libAtomVM/bifs.gperf +++ b/src/libAtomVM/bifs.gperf @@ -41,6 +41,7 @@ erlang:byte_size/1, {.gcbif.base.type = GCBIFFunctionType, .gcbif.gcbif1_ptr = b erlang:bit_size/1, {.gcbif.base.type = GCBIFFunctionType, .gcbif.gcbif1_ptr = bif_erlang_bit_size_1} erlang:get/1, {.bif.base.type = BIFFunctionType, .bif.bif1_ptr = bif_erlang_get_1} erlang:is_atom/1, {.bif.base.type = BIFFunctionType, .bif.bif1_ptr = bif_erlang_is_atom_1} +erlang:is_bitstring/1, {.bif.base.type = BIFFunctionType, .bif.bif1_ptr = bif_erlang_is_binary_1} erlang:is_binary/1, {.bif.base.type = BIFFunctionType, .bif.bif1_ptr = bif_erlang_is_binary_1} erlang:is_boolean/1, {.bif.base.type = BIFFunctionType, .bif.bif1_ptr = bif_erlang_is_boolean_1} erlang:is_float/1, {.bif.base.type = BIFFunctionType, .bif.bif1_ptr = bif_erlang_is_float_1} diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 117aff6f9..0ed97aba7 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -96,7 +96,7 @@ static term nif_binary_part_3(Context *ctx, int argc, term argv[]); static term nif_binary_split_2(Context *ctx, int argc, term argv[]); static term nif_calendar_system_time_to_universal_time_2(Context *ctx, int argc, term argv[]); static term nif_erlang_delete_element_2(Context *ctx, int argc, term argv[]); -static term nif_erlang_atom_to_binary_2(Context *ctx, int argc, term argv[]); +static term nif_erlang_atom_to_binary(Context *ctx, int argc, term argv[]); static term nif_erlang_atom_to_list_1(Context *ctx, int argc, term argv[]); static term nif_erlang_binary_to_atom_2(Context *ctx, int argc, term argv[]); static term nif_erlang_binary_to_float_1(Context *ctx, int argc, term argv[]); @@ -252,7 +252,7 @@ static const struct Nif make_ref_nif = static const struct Nif atom_to_binary_nif = { .base.type = NIFFunctionType, - .nif_ptr = nif_erlang_atom_to_binary_2 + .nif_ptr = nif_erlang_atom_to_binary }; static const struct Nif atom_to_list_nif = @@ -2121,14 +2121,12 @@ term list_to_atom(Context *ctx, int argc, term argv[], int create_new) return term_from_atom_index(global_atom_index); } -static term nif_erlang_atom_to_binary_2(Context *ctx, int argc, term argv[]) +static term nif_erlang_atom_to_binary(Context *ctx, int argc, term argv[]) { - UNUSED(argc); - term atom_term = argv[0]; VALIDATE_VALUE(atom_term, term_is_atom); - term encoding = argv[1]; + term encoding = (argc == 1) ? UTF8_ATOM : argv[1]; GlobalContext *glb = ctx->global; @@ -4869,7 +4867,10 @@ static term nif_unicode_characters_to_list(Context *ctx, int argc, term argv[]) } size_t len = size / sizeof(uint32_t); uint32_t *chars = malloc(size); - if (IS_NULL_PTR(chars)) { + // fun fact: malloc(size) when size is 0, on some platforms may return NULL, causing a failure here + // so in order to avoid out_of_memory (while having plenty of memory) let's treat size==0 as a + // special case + if (UNLIKELY((chars == NULL) && (size != 0))) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } size_t needed_terms = CONS_SIZE * len; @@ -4933,7 +4934,7 @@ static term nif_unicode_characters_to_binary(Context *ctx, int argc, term argv[] if (UNLIKELY(conv_result == UnicodeBadArg)) { RAISE_ERROR(BADARG_ATOM); } - size_t needed_terms = term_binary_data_size_in_terms(len); + size_t needed_terms = term_binary_heap_size(len); if (UNLIKELY(conv_result == UnicodeError || conv_result == UnicodeIncompleteTransform)) { needed_terms += TUPLE_SIZE(3) + rest_size; } diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index 86c523599..4a812b8ee 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -37,6 +37,7 @@ binary:last/1, &binary_last_nif binary:part/3, &binary_part_nif binary:split/2, &binary_split_nif calendar:system_time_to_universal_time/2, &system_time_to_universal_time_nif +erlang:atom_to_binary/1, &atom_to_binary_nif erlang:atom_to_binary/2, &atom_to_binary_nif erlang:atom_to_list/1, &atom_to_list_nif erlang:binary_to_atom/1, &binary_to_atom_nif diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 3fd95da72..21ab62378 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -145,10 +145,17 @@ typedef dreg_t dreg_gc_safe_t; dest_term = term_from_int(((first_byte & 0xE0) << 3) | *(decode_pc)++); \ break; \ \ - default: \ - fprintf(stderr, "Operand not literal: %x, or unsupported encoding\n", (first_byte)); \ - AVM_ABORT(); \ + case 3: { \ + uint8_t sz = (first_byte >> 5) + 2; \ + avm_int_t val = 0; \ + for (uint8_t vi = 0; vi < sz; vi++) { \ + val <<= 8; \ + val |= *(decode_pc)++; \ + } \ + dest_term = term_from_int(val); \ break; \ + } \ + default: UNREACHABLE(); /* help gcc 8.4 */ \ } \ break; \ \ @@ -549,10 +556,17 @@ static void destroy_extended_registers(Context *ctx, unsigned int live) dest_term = term_from_int(((first_byte & 0xE0) << 3) | *(decode_pc)++); \ break; \ \ - default: \ - fprintf(stderr, "Operand not a literal: %x, or unsupported encoding\n", (first_byte)); \ - AVM_ABORT(); \ + case 3: { \ + uint8_t sz = (first_byte >> 5) + 2; \ + avm_int_t val = 0; \ + for (uint8_t vi = 0; vi < sz; vi++) { \ + val <<= 8; \ + val |= *(decode_pc)++; \ + } \ + dest_term = term_from_int(val); \ break; \ + } \ + default: UNREACHABLE(); /* help gcc 8.4 */ \ } \ break; \ \ @@ -5363,7 +5377,9 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) #ifdef IMPL_EXECUTE_LOOP TRACE("is_bitstr/2, label=%i, arg1=%lx\n", label, arg1); - pc = mod->labels[label]; + if (!term_is_binary(arg1)) { + pc = mod->labels[label]; + } #endif #ifdef IMPL_CODE_LOADER diff --git a/src/platforms/esp32/components/avm_builtins/gpio_driver.c b/src/platforms/esp32/components/avm_builtins/gpio_driver.c index 7dcabf1f0..a0c3202c1 100644 --- a/src/platforms/esp32/components/avm_builtins/gpio_driver.c +++ b/src/platforms/esp32/components/avm_builtins/gpio_driver.c @@ -422,10 +422,10 @@ static term gpiodriver_set_int(Context *ctx, int32_t target_pid, term cmd) struct GPIOData *gpio_data = ctx->platform_data; - term gpio_num_term = term_to_int32(term_get_tuple_element(cmd, 1)); + term gpio_num_term = term_get_tuple_element(cmd, 1); gpio_num_t gpio_num; if (LIKELY(term_is_integer(gpio_num_term))) { - avm_int_t pin_int = term_to_int32(gpio_num_term); + int32_t pin_int = term_to_int32(gpio_num_term); if (UNLIKELY((pin_int < 0) || (pin_int >= GPIO_NUM_MAX))) { return ERROR_ATOM; } @@ -525,10 +525,10 @@ static term gpiodriver_remove_int(Context *ctx, term cmd) { struct GPIOData *gpio_data = ctx->platform_data; - term gpio_num_term = term_to_int32(term_get_tuple_element(cmd, 1)); + term gpio_num_term = term_get_tuple_element(cmd, 1); gpio_num_t gpio_num; if (LIKELY(term_is_integer(gpio_num_term))) { - avm_int_t pin_int = term_to_int32(gpio_num_term); + int32_t pin_int = term_to_int32(gpio_num_term); if (UNLIKELY((pin_int < 0) || (pin_int >= GPIO_NUM_MAX))) { return ERROR_ATOM; } diff --git a/src/platforms/esp32/partitions-elixir.csv b/src/platforms/esp32/partitions-elixir.csv new file mode 100644 index 000000000..3c8b47dc6 --- /dev/null +++ b/src/platforms/esp32/partitions-elixir.csv @@ -0,0 +1,12 @@ +# Copyright 2018-2021 Davide Bettio +# Copyright 2018-2021 Fred Dushin +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + +# Name, Type, SubType, Offset, Size, Flags +# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 0x1C0000, +boot.avm, data, phy, 0x1D0000, 0x80000, +main.avm, data, phy, 0x250000, 0x100000 diff --git a/src/platforms/esp32/tools/mkimage.config.in b/src/platforms/esp32/tools/mkimage.config.in index bd5d109c5..8856f72a1 100644 --- a/src/platforms/esp32/tools/mkimage.config.in +++ b/src/platforms/esp32/tools/mkimage.config.in @@ -38,7 +38,7 @@ #{ name => "AtomVM Boot and Core BEAM Library", offset => "0x1D0000", - path => ["${BUILD_DIR}/../../../../build/libs/esp32boot/esp32boot.avm"] + path => ["$[BOOT_FILE]"] } ] }. diff --git a/src/platforms/esp32/tools/mkimage.erl b/src/platforms/esp32/tools/mkimage.erl index ce198b826..fd9d73ed2 100644 --- a/src/platforms/esp32/tools/mkimage.erl +++ b/src/platforms/esp32/tools/mkimage.erl @@ -42,9 +42,12 @@ do_main(Argv) -> RootDir -> try Config = load_config(maps:get(config, Opts, "mkimage.config")), + BuildDir = get_build_dir(Opts, RootDir), + BootFile = BuildDir ++ "/libs/esp32boot/esp32boot.avm", mkimage( RootDir, - get_build_dir(Opts, RootDir), + BuildDir, + maps:get(boot, Opts, BootFile), maps:get(out, Opts, "atomvm.img"), maps:get(segments, Config) ), @@ -65,6 +68,8 @@ parse_args(Argv) -> %% @private parse_args([], {Opts, Args}) -> {Opts, lists:reverse(Args)}; +parse_args(["--boot", Path | T], {Opts, Args}) -> + parse_args(T, {Opts#{boot => Path}, Args}); parse_args(["--out", Path | T], {Opts, Args}) -> parse_args(T, {Opts#{out => Path}, Args}); parse_args(["--root_dir", Path | T], {Opts, Args}) -> @@ -92,6 +97,7 @@ print_help() -> "The following options are supported:" "~n" " * --root_dir Path to the root directory of the AtomVM git checkout~n" + " * --boot Path to a esp32boot.avm file~n" " * --build_dir Path to the AtomVM build directory (defaults to root_dir/build, if unspecifeid)~n" " * --out Output path for AtomVM image file~n" " * --config Path to mkimage configuration file~n" @@ -124,7 +130,7 @@ get_build_dir(Opts, RootDir) -> end. %% @private -mkimage(RootDir, BuildDir, OutputFile, Segments) -> +mkimage(RootDir, BuildDir, BootFile, OutputFile, Segments) -> io:format("Writing output to ~s~n", [OutputFile]), io:format("=============================================~n"), case file:open(OutputFile, [write, binary]) of @@ -156,7 +162,13 @@ mkimage(RootDir, BuildDir, OutputFile, Segments) -> end end, SegmentPaths = [ - replace("BUILD_DIR", BuildDir, replace("ROOT_DIR", RootDir, SegmentPath)) + replace( + "BUILD_DIR", + BuildDir, + replace( + "BOOT_FILE", BootFile, replace("ROOT_DIR", RootDir, SegmentPath) + ) + ) || SegmentPath <- maps:get(path, Segment) ], case try_read(SegmentPaths) of @@ -200,4 +212,5 @@ from_hex([$0, $x | Bits]) -> %% @private replace(VariableName, Value, String) -> - string:replace(String, io_lib:format("${~s}", [VariableName]), Value). + string:replace(String, io_lib:format("${~s}", [VariableName]), Value), + string:replace(String, io_lib:format("$[~s]", [VariableName]), Value). diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 1fc7db541..c99c72f96 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -143,6 +143,7 @@ compile_erlang(test_list_to_integer) compile_erlang(test_abs) compile_erlang(test_is_process_alive) compile_erlang(test_is_not_type) +compile_erlang(test_is_bitstring_is_binary) compile_erlang(test_badarith) compile_erlang(test_badarith2) compile_erlang(test_badarith3) @@ -320,6 +321,7 @@ compile_erlang(minuspow63plusoneabs) compile_erlang(minuspow63plustwoabs) compile_erlang(literal_test0) compile_erlang(literal_test1) +compile_erlang(literal_test2) compile_erlang(test_list_eq) compile_erlang(test_tuple_eq) @@ -610,6 +612,7 @@ add_custom_target(erlang_test_modules DEPENDS test_abs.beam test_is_process_alive.beam test_is_not_type.beam + test_is_bitstring_is_binary.beam test_badarith.beam test_badarith2.beam test_badarith3.beam @@ -789,6 +792,7 @@ add_custom_target(erlang_test_modules DEPENDS literal_test0.beam literal_test1.beam + literal_test2.beam test_list_eq.beam test_tuple_eq.beam diff --git a/tests/erlang_tests/literal_test2.erl b/tests/erlang_tests/literal_test2.erl new file mode 100644 index 000000000..36dd69945 --- /dev/null +++ b/tests/erlang_tests/literal_test2.erl @@ -0,0 +1,67 @@ +% +% This file is part of AtomVM. +% +% Copyright 2024 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(literal_test2). +-export([start/0, f/1, g/1]). + +start() -> + Result = f(<<"duh">>), + <<"1234567890abcdef01234567890abcdef01234567890abcdef01234567890abcdef0duh">> = Result, + 0. + +% This is large enough to have to decode literal index with last encoding. +g(X) -> + << + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0" + "1234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0", + X/binary + >>. + +f(X) -> + <<"1234567890abcdef01234567890abcdef01234567890abcdef01234567890abcdef0", X/binary>>. diff --git a/tests/erlang_tests/test_is_bitstring_is_binary.erl b/tests/erlang_tests/test_is_bitstring_is_binary.erl new file mode 100644 index 000000000..e727c6ff1 --- /dev/null +++ b/tests/erlang_tests/test_is_bitstring_is_binary.erl @@ -0,0 +1,50 @@ +% +% This file is part of AtomVM. +% +% Copyright 2024 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_is_bitstring_is_binary). + +-export([start/0, id/1]). + +start() -> + test_is_bitstring(), + test_is_binary(), + 0. + +id(X) -> X. + +test_is_bitstring() -> + true = is_bitstring(id(<<"hello">>)), + % bitstrings are currently unsupported + % true = is_bitstring(id(<<1:1>>)), + true = is_bitstring(id(<<>>)), + false = is_bitstring(id(binary)), + false = is_bitstring(id("hello")), + false = is_bitstring(id(42)), + ok. + +test_is_binary() -> + true = is_binary(id(<<"hello">>)), + % bitstrings are currently unsupported + % false = is_binary(id(<<1:1>>)), + true = is_binary(id(<<>>)), + false = is_binary(id(binary)), + false = is_binary(id("hello")), + false = is_binary(id(42)), + ok. diff --git a/tests/libs/estdlib/test_gen_server.erl b/tests/libs/estdlib/test_gen_server.erl index bf88d1b16..14a6396af 100644 --- a/tests/libs/estdlib/test_gen_server.erl +++ b/tests/libs/estdlib/test_gen_server.erl @@ -21,7 +21,7 @@ -module(test_gen_server). -export([test/0]). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). +-export([init/1, handle_continue/2, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). -record(state, { num_casts = 0, @@ -36,6 +36,8 @@ test() -> ok = test_cast(), ok = test_info(), ok = test_start_link(), + ok = test_start_monitor(), + ok = test_continue(), ok = test_init_exception(), ok = test_late_reply(), ok = test_concurrent_clients(), @@ -77,6 +79,68 @@ test_start_link() -> true = erlang:process_flag(trap_exit, false), ok. +test_start_monitor() -> + case get_otp_version() of + Version when Version =:= atomvm orelse (is_integer(Version) andalso Version >= 23) -> + {ok, {Pid, Ref}} = gen_server:start_monitor(?MODULE, [], []), + + pong = gen_server:call(Pid, ping), + pong = gen_server:call(Pid, reply_ping), + ok = gen_server:cast(Pid, crash), + ok = + receive + {'DOWN', Ref, process, Pid, _Reason} -> ok + after 30000 -> timeout + end, + ok; + _ -> + ok + end. + +test_continue() -> + {ok, Pid} = gen_server:start_link(?MODULE, {continue, self()}, []), + [{Pid, continue}, {Pid, after_continue}] = read_replies(Pid), + + gen_server:call(Pid, {continue_reply, self()}), + [{Pid, continue}, {Pid, after_continue}] = read_replies(Pid), + + gen_server:call(Pid, {continue_noreply, self()}), + [{Pid, continue}, {Pid, after_continue}] = read_replies(Pid), + + gen_server:cast(Pid, {continue_noreply, self()}), + [{Pid, continue}, {Pid, after_continue}] = read_replies(Pid), + + Pid ! {continue_noreply, self()}, + [{Pid, continue}, {Pid, after_continue}] = read_replies(Pid), + + Pid ! {continue_continue, self()}, + [{Pid, before_continue}, {Pid, continue}, {Pid, after_continue}] = read_replies(Pid), + + Ref = monitor(process, Pid), + Pid ! continue_stop, + verify_down_reason(Ref, Pid, normal). + +read_replies(Pid) -> + receive + {Pid, ack} -> read_replies() + after 1000 -> + error + end. + +read_replies() -> + receive + Msg -> [Msg | read_replies()] + after 0 -> [] + end. + +verify_down_reason(MRef, Server, Reason) -> + receive + {'DOWN', MRef, process, Server, Reason} -> + ok + after 5000 -> + error + end. + test_cast() -> {ok, Pid} = gen_server:start(?MODULE, [], []), @@ -347,17 +411,49 @@ test_stop_noproc() -> ok end. +get_otp_version() -> + case erlang:system_info(machine) of + "BEAM" -> + list_to_integer(erlang:system_info(otp_release)); + _ -> + atomvm + end. + %% %% callbacks %% init(throwme) -> throw(throwme); +init({continue, Pid}) -> + io:format("init(continue) -> ~p~n", [Pid]), + self() ! {after_continue, Pid}, + {ok, [], {continue, {message, Pid}}}; init(_) -> {ok, #state{}}. +handle_continue({continue, Pid}, State) -> + Pid ! {self(), before_continue}, + self() ! {after_continue, Pid}, + {noreply, State, {continue, {message, Pid}}}; +handle_continue(stop, State) -> + {stop, normal, State}; +handle_continue({message, Pid}, State) -> + Pid ! {self(), continue}, + {noreply, State}; +handle_continue({message, Pid, From}, State) -> + Pid ! {self(), continue}, + gen_server:reply(From, ok), + {noreply, State}. + handle_call(ping, _From, State) -> {reply, pong, State}; +handle_call({continue_reply, Pid}, _From, State) -> + self() ! {after_continue, Pid}, + {reply, ok, State, {continue, {message, Pid}}}; +handle_call({continue_noreply, Pid}, From, State) -> + self() ! {after_continue, Pid}, + {noreply, State, {continue, {message, Pid, From}}}; handle_call(reply_ping, From, State) -> gen_server:reply(From, pong), {noreply, State}; @@ -392,6 +488,9 @@ handle_call(crash_me, _From, State) -> handle_call(crash_in_terminate, _From, State) -> {reply, ok, State#state{crash_in_terminate = true}}. +handle_cast({continue_noreply, Pid}, State) -> + self() ! {after_continue, Pid}, + {noreply, State, {continue, {message, Pid}}}; handle_cast(crash, _State) -> throw(test_crash); handle_cast(ping, #state{num_casts = NumCasts} = State) -> @@ -403,6 +502,17 @@ handle_cast({set_info_timeout, Timeout}, State) -> handle_cast(_Request, State) -> {noreply, State}. +handle_info({after_continue, Pid}, State) -> + Pid ! {self(), after_continue}, + Pid ! {self(), ack}, + {noreply, State}; +handle_info(continue_stop, State) -> + {noreply, State, {continue, stop}}; +handle_info({continue_noreply, Pid}, State) -> + self() ! {after_continue, Pid}, + {noreply, State, {continue, {message, Pid}}}; +handle_info({continue_continue, Pid}, State) -> + {noreply, State, {continue, {continue, Pid}}}; handle_info(ping, #state{num_infos = NumInfos, info_timeout = InfoTimeout} = State) -> NewState = State#state{num_infos = NumInfos + 1}, case InfoTimeout of diff --git a/tests/libs/estdlib/test_lists.erl b/tests/libs/estdlib/test_lists.erl index bb7350e65..133a33f2c 100644 --- a/tests/libs/estdlib/test_lists.erl +++ b/tests/libs/estdlib/test_lists.erl @@ -46,6 +46,8 @@ test() -> ok = test_split(), ok = test_usort(), ok = test_filtermap(), + ok = test_last(), + ok = test_mapfoldl(), ok. test_nth() -> @@ -296,4 +298,17 @@ test_filtermap() -> ), ok. +test_last() -> + ?ASSERT_ERROR(lists:last([]), function_clause), + ?ASSERT_MATCH(a, lists:last([a])), + ?ASSERT_MATCH(b, lists:last([a, b])), + ?ASSERT_ERROR(lists:last([a | b]), function_clause), + ok. + +test_mapfoldl() -> + ?ASSERT_MATCH({[], 1}, lists:mapfoldl(fun(X, A) -> {X * A, A + 1} end, 1, [])), + ?ASSERT_MATCH({[1, 4, 9], 4}, lists:mapfoldl(fun(X, A) -> {X * A, A + 1} end, 1, [1, 2, 3])), + ?ASSERT_ERROR(lists:mapfoldl(fun(X, A) -> {X * A, A + 1} end, 1, foo), function_clause), + ok. + id(X) -> X. diff --git a/tests/libs/estdlib/test_maps.erl b/tests/libs/estdlib/test_maps.erl index 32e45d440..38439b7d2 100644 --- a/tests/libs/estdlib/test_maps.erl +++ b/tests/libs/estdlib/test_maps.erl @@ -56,6 +56,19 @@ test() -> ok = test_foreach(), ok = test_map(), ok = test_merge(), + HasMergeWith = + case erlang:system_info(machine) of + "BEAM" -> + erlang:system_info(version) >= "12.3"; + "ATOM" -> + true + end, + case HasMergeWith of + true -> + ok = test_merge_with(); + false -> + ok + end, ok = test_remove(), ok = test_update(), ok. @@ -284,6 +297,43 @@ test_merge() -> ok = check_bad_map(fun() -> maps:merge(id(not_a_map), maps:new()) end), ok. +test_merge_with() -> + ?ASSERT_EQUALS(maps:merge_with(fun(_K, V1, V2) -> V1 + V2 end, maps:new(), maps:new()), #{}), + ?ASSERT_EQUALS( + maps:merge_with(fun(_K, V1, V2) -> V1 + V2 end, #{a => 1, b => 2, c => 3}, maps:new()), #{ + a => 1, b => 2, c => 3 + } + ), + ?ASSERT_EQUALS( + maps:merge_with(fun(_K, V1, V2) -> V1 + V2 end, maps:new(), #{a => 1, b => 2, c => 3}), #{ + a => 1, b => 2, c => 3 + } + ), + ?ASSERT_EQUALS( + maps:merge_with(fun(_K, V1, V2) -> V1 + V2 end, #{a => 1, b => 2, d => 4}, #{ + a => 1, b => 2, c => 3 + }), + #{a => 2, b => 4, c => 3, d => 4} + ), + ?ASSERT_EQUALS( + maps:merge_with(fun(_K, V1, V2) -> {V1, V2} end, #{a => 1, b => 2, c => 3}, #{ + b => z, d => 4 + }), + #{ + a => 1, + b => {2, z}, + c => 3, + d => 4 + } + ), + ok = check_bad_map(fun() -> + maps:merge_with(fun(_K, V1, V2) -> V1 + V2 end, maps:new(), id(not_a_map)) + end), + ok = check_bad_map(fun() -> + maps:merge_with(fun(_K, V1, V2) -> V1 + V2 end, id(not_a_map), maps:new()) + end), + ok. + test_remove() -> ?ASSERT_EQUALS(maps:remove(foo, maps:new()), #{}), ?ASSERT_EQUALS(maps:remove(a, #{a => 1, b => 2, c => 3}), #{b => 2, c => 3}), diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 44ae8d2a1..fe34c30e8 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -19,6 +19,10 @@ # defmodule Tests do + # defstruct [ + # :field1, + # field2: 42 + # ] @compile {:no_warn_undefined, :undef} @@ -26,6 +30,8 @@ defmodule Tests do :ok = IO.puts("Running Elixir tests") :ok = test_enum() :ok = test_exception() + :ok = test_chars_protocol() + :ok = test_inspect() :ok = IO.puts("Finished Elixir tests") end @@ -37,6 +43,15 @@ defmodule Tests do [0, 2, 4] = Enum.map([0, 1, 2], fn x -> x * 2 end) 6 = Enum.reduce([1, 2, 3], 0, fn x, acc -> acc + x end) [2, 3] = Enum.slice([1, 2, 3], 1, 2) + :test = Enum.at([0, 1, :test, 3], 2) + :atom = Enum.find([1, 2, :atom, 3, 4], -1, fn item -> not is_integer(item) end) + 1 = Enum.find_index([:a, :b, :c], fn item -> item == :b end) + true = Enum.find_value([-2, -3, -1, 0, 1], fn item -> item >= 0 end) + true = Enum.all?([1, 2, 3], fn n -> n >= 0 end) + true = Enum.any?([1, -2, 3], fn n -> n < 0 end) + [2] = Enum.filter([1, 2, 3], fn n -> rem(n, 2) == 0 end) + [1, 3] = Enum.reject([1, 2, 3], fn n -> rem(n, 2) == 0 end) + :ok = Enum.each([1, 2, 3], fn n -> true = is_integer(n) end) # map 2 = Enum.count(%{a: 1, b: 2}) @@ -49,7 +64,20 @@ defmodule Tests do :a = kw2[:A] :b = kw2[:B] kw3 = Enum.slice(%{a: 1, b: 2}, 0, 1) - true = (length(kw3) == 1) and ((kw3[:a] == 1) or (kw3[:b] == 2)) + true = length(kw3) == 1 and (kw3[:a] == 1 or kw3[:b] == 2) + at_0 = Enum.at(%{a: 1, b: 2}, 0) + at_1 = Enum.at(%{a: 1, b: 2}, 1) + true = at_0 == {:a, 1} or at_0 == {:b, 2} + true = at_1 == {:a, 1} or at_1 == {:b, 2} + true = at_0 != at_1 + {:c, :atom} = Enum.find(%{a: 1, b: 2, c: :atom, d: 3}, fn {_k, v} -> not is_integer(v) end) + {:d, 3} = Enum.find(%{a: 1, b: 2, c: :atom, d: 3}, fn {k, _v} -> k == :d end) + true = Enum.find_value(%{"a" => 1, b: 2}, fn {k, _v} -> is_atom(k) end) + true = Enum.all?(%{a: 1, b: 2}, fn {_k, v} -> v >= 0 end) + true = Enum.any?(%{a: 1, b: -2}, fn {_k, v} -> v < 0 end) + [b: 2] = Enum.filter(%{a: 1, b: 2, c: 3}, fn {_k, v} -> rem(v, 2) == 0 end) + [] = Enum.reject(%{a: 1, b: 2, c: 3}, fn {_k, v} -> v > 0 end) + :ok = Enum.each(%{a: 1, b: 2}, fn {_k, v} -> true = is_integer(v) end) # map set 3 = Enum.count(MapSet.new([0, 1, 2])) @@ -58,6 +86,17 @@ defmodule Tests do [0, 2, 4] = Enum.map(MapSet.new([0, 1, 2]), fn x -> x * 2 end) 6 = Enum.reduce(MapSet.new([1, 2, 3]), 0, fn x, acc -> acc + x end) [] = Enum.slice(MapSet.new([1, 2]), 1, 0) + ms_at_0 = Enum.at(MapSet.new([1, 2]), 0) + ms_at_1 = Enum.at(MapSet.new([1, 2]), 1) + true = ms_at_0 == 1 or ms_at_0 == 2 + true = ms_at_1 == 1 or ms_at_1 == 2 + :atom = Enum.find(MapSet.new([1, 2, :atom, 3, 4]), fn item -> not is_integer(item) end) + nil = Enum.find_value([-2, -3, -1, 0, 1], fn item -> item > 100 end) + true = Enum.all?(MapSet.new([1, 2, 3]), fn n -> n >= 0 end) + true = Enum.any?(MapSet.new([1, -2, 3]), fn n -> n < 0 end) + [2] = Enum.filter(MapSet.new([1, 2, 3]), fn n -> rem(n, 2) == 0 end) + [1] = Enum.reject(MapSet.new([1, 2, 3]), fn n -> n > 1 end) + :ok = Enum.each(MapSet.new([1, 2, 3]), fn n -> true = is_integer(n) end) # range 4 = Enum.count(1..4) @@ -66,6 +105,14 @@ defmodule Tests do [1, 2, 3, 4] = Enum.map(1..4, fn x -> x end) 55 = Enum.reduce(1..10, 0, fn x, acc -> x + acc end) [6, 7, 8, 9, 10] = Enum.slice(1..10, 5, 100) + 7 = Enum.at(1..10, 6) + 8 = Enum.find(-10..10, fn item -> item >= 8 end) + true = Enum.find_value(-10..10, fn item -> item >= 0 end) + true = Enum.all?(0..10, fn n -> n >= 0 end) + true = Enum.any?(-1..10, fn n -> n < 0 end) + [0, 1, 2] = Enum.filter(-10..2, fn n -> n >= 0 end) + [-1] = Enum.reject(-1..10, fn n -> n >= 0 end) + :ok = Enum.each(-5..5, fn n -> true = is_integer(n) end) # into %{a: 1, b: 2} = Enum.into([a: 1, b: 2], %{}) @@ -73,12 +120,22 @@ defmodule Tests do expected_mapset = MapSet.new([1, 2, 3]) ^expected_mapset = Enum.into([1, 2, 3], MapSet.new()) + # Enum.flat_map + [:a, :a, :b, :b, :c, :c] = Enum.flat_map([:a, :b, :c], fn x -> [x, x] end) + [1, 2, 3, 4, 5, 6] = Enum.flat_map([{1, 3}, {4, 6}], fn {x, y} -> x..y end) + [[:a], [:b], [:c]] = Enum.flat_map([:a, :b, :c], fn x -> [[x]] end) + # Enum.join "1, 2, 3" = Enum.join(["1", "2", "3"], ", ") + "1, 2, 3" = Enum.join([1, 2, 3], ", ") + "123" = Enum.join([1, 2, 3], "") # Enum.reverse [4, 3, 2] = Enum.reverse([2, 3, 4]) + # other enum functions + test_enum_chunk_while() + undef = try do Enum.map({1, 2}, fn x -> x end) @@ -98,6 +155,28 @@ defmodule Tests do :ok end + defp test_enum_chunk_while() do + initial_col = 4 + lines_list = '-1234567890\nciao\n12345\nabcdefghijkl\n12' + columns = 5 + + chunk_fun = fn char, {count, rchars} -> + cond do + char == ?\n -> {:cont, Enum.reverse(rchars), {0, []}} + count == columns -> {:cont, Enum.reverse(rchars), {1, [char]}} + true -> {:cont, {count + 1, [char | rchars]}} + end + end + + after_fun = fn + {_count, []} -> {:cont, [], []} + {_count, rchars} -> {:cont, Enum.reverse(rchars), []} + end + + ['-', '12345', '67890', 'ciao', '12345', 'abcde', 'fghij', 'kl', '12'] = + Enum.chunk_while(lines_list, {initial_col, []}, chunk_fun, after_fun) + end + defp test_exception() do ex1 = try do @@ -150,7 +229,73 @@ defmodule Tests do :ok end + def test_chars_protocol() do + "" = String.Chars.to_string(nil) + "hello" = String.Chars.to_string(:hello) + "hellø" = String.Chars.to_string(:hellø) + "123" = String.Chars.to_string(123) + "1.0" = String.Chars.to_string(1.0) + "abc" = String.Chars.to_string(~c"abc") + "test" = String.Chars.to_string("test") + :ok + end + + def test_inspect() do + "true" = inspect(true) + "false" = inspect(false) + "nil" = inspect(nil) + + ":test" = inspect(:test) + ":アトム" = inspect(:アトム) + "Test" = inspect(Test) + + "5" = inspect(5) + "5.0" = inspect(5.0) + + ~s[""] = inspect("") + ~s["hello"] = inspect("hello") + ~s["アトム"] = inspect("アトム") + + "<<10>>" = inspect("\n") + "<<0, 1, 2, 3>>" = inspect(<<0, 1, 2, 3>>) + "<<195, 168, 0>>" = inspect(<<195, 168, 0>>) + + "[]" = inspect([]) + "[0]" = inspect([0]) + "[9, 10]" = inspect([9, 10]) + ~s'["test"]' = inspect(["test"]) + "'hello'" = inspect('hello') + "[127]" = inspect([127]) + "[104, 101, 108, 108, 248]" = inspect('hellø') + + ~s([5 | "hello"]) = inspect([5 | "hello"]) + + "{}" = inspect({}) + "{1, 2}" = inspect({1, 2}) + "{:test, 1}" = inspect({:test, 1}) + + "%{}" = inspect(%{}) + either("%{a: 1, b: 2}", "%{b: 2, a: 1}", inspect(%{a: 1, b: 2})) + either(~s[%{"a" => 1, "b" => 2}], ~s[%{"b" => 2, "a" => 1}], inspect(%{"a" => 1, "b" => 2})) + + # TODO: structs are not yet supported + # either( + # ~s[%#{__MODULE__}{field1: nil, field2: 42}], + # ~s[%#{__MODULE__}{field2: 42, field1: nil}], + # inspect(%__MODULE__{}) + # ) + + :ok + end + defp fact(n) when n < 0, do: :test defp fact(0), do: 1 defp fact(n), do: fact(n - 1) * n + + def either(a, b, value) do + case value do + ^a -> a + ^b -> b + end + end end diff --git a/tests/test.c b/tests/test.c index f470d13b5..df0515689 100644 --- a/tests/test.c +++ b/tests/test.c @@ -75,6 +75,34 @@ struct Test #define SKIP_STACKTRACES false #endif +// Enabling this will override malloc and calloc weak symbols, +// so we can force an alternative version of malloc that returns +// NULL when size is 0. +// This is useful to find debugging or finding some kind of issues. +#ifdef FORCE_MALLOC_ZERO_RETURNS_NULL +void *malloc(size_t size) +{ + if (size == 0) { + return NULL; + } else { + void *memptr = NULL; + if (posix_memalign(&memptr, sizeof(void *), size) != 0) { + return NULL; + } + return memptr; + } +} + +void *calloc(size_t nmemb, size_t size) +{ + void *ptr = malloc(nmemb * size); + if (ptr != NULL) { + memset(ptr, 0, nmemb * size); + } + return ptr; +} +#endif + struct Test tests[] = { TEST_CASE_EXPECTED(add, 17), TEST_CASE_EXPECTED(fact, 120), @@ -166,6 +194,7 @@ struct Test tests[] = { TEST_CASE_EXPECTED(test_abs, 5), TEST_CASE_EXPECTED(test_is_process_alive, 121), TEST_CASE_EXPECTED(test_is_not_type, 255), + TEST_CASE(test_is_bitstring_is_binary), TEST_CASE_EXPECTED(test_badarith, -87381), TEST_CASE_EXPECTED(test_badarith2, -87381), TEST_CASE_EXPECTED(test_badarith3, -1365), @@ -335,6 +364,7 @@ struct Test tests[] = { TEST_CASE_EXPECTED(literal_test0, 333575620), TEST_CASE_EXPECTED(literal_test1, 1680), + TEST_CASE(literal_test2), TEST_CASE_EXPECTED(test_list_eq, 1), TEST_CASE_EXPECTED(test_tuple_eq, 1),