From dfccb2e25e05314279134ae7da63aba62500d9b6 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 18 Aug 2024 11:14:07 +0200 Subject: [PATCH 01/38] Prepare v0.6.5 dev cycle Signed-off-by: Davide Bettio --- CHANGELOG.md | 2 ++ src/libAtomVM/atomvm_version.h | 2 +- version.cmake | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c041b0c3c..1c1b1793e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.5] - Unreleased + ## [0.6.4] - 2024-08-18 ### Added diff --git a/src/libAtomVM/atomvm_version.h b/src/libAtomVM/atomvm_version.h index 51fbfa84b..8f97c4a2b 100644 --- a/src/libAtomVM/atomvm_version.h +++ b/src/libAtomVM/atomvm_version.h @@ -23,6 +23,6 @@ #define ATOMVM_VERSION_MAJOR 0 #define ATOMVM_VERSION_MINOR 6 -#define ATOMVM_VERSION_PATCH 4 +#define ATOMVM_VERSION_PATCH 5 #endif diff --git a/version.cmake b/version.cmake index 4924e75dc..0de8e7d8e 100644 --- a/version.cmake +++ b/version.cmake @@ -19,5 +19,5 @@ # # Please, keep also in sync src/libAtomVM/atomvm_version.h -set(ATOMVM_BASE_VERSION "0.6.4") -set(ATOMVM_DEV FALSE) +set(ATOMVM_BASE_VERSION "0.6.5") +set(ATOMVM_DEV TRUE) From 97caac1f3dd9260b7757024e2886c057e422ec08 Mon Sep 17 00:00:00 2001 From: Winford Date: Sun, 18 Aug 2024 15:32:18 -0700 Subject: [PATCH 02/38] Fix typo in docs/conf.py.in Makes `omit_branch_list` a proper list, so items added in the future will not cause errors. Signed-off-by: Winford --- doc/conf.py.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 2a18e6f67d154e56e3ea8dbbda50494f6e396f84 Mon Sep 17 00:00:00 2001 From: Winford Date: Sun, 18 Aug 2024 15:33:50 -0700 Subject: [PATCH 03/38] Correction to publish_docs workflow for branches The github `actions/checkout@v4` only tracks the current branch, even when `fetch-depth: 0` is used, this causes branch names other than the current one to be missed when creating the html navigation menus. This is solved by adding a step to track all of the remote branches that were fetched during checkout. Signed-off-by: Winford --- .github/workflows/build-docs.yaml | 8 ++++++++ .github/workflows/publish-docs.yaml | 8 ++++++++ 2 files changed, 16 insertions(+) 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/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: | From 229ad99944b21086b6e2dd18d5fe1c21f61ff569 Mon Sep 17 00:00:00 2001 From: Winford Date: Wed, 5 Jun 2024 18:34:02 -0700 Subject: [PATCH 04/38] Run ESP32 workflows run on sub-directory changes too Updates the ESP32 workflows to run on changes inside sub-directories. Signed-off-by: Winford --- .github/workflows/esp32-build.yaml | 2 ++ .github/workflows/esp32-mkimage.yaml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/esp32-build.yaml b/.github/workflows/esp32-build.yaml index 0f8650976..eeff68cd7 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: diff --git a/.github/workflows/esp32-mkimage.yaml b/.github/workflows/esp32-mkimage.yaml index e938ab58c..83bb26f2c 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: From 5b3f4f3e571989a6cff3f57a3dc840541f432aab Mon Sep 17 00:00:00 2001 From: Winford Date: Sun, 4 Aug 2024 19:22:11 -0700 Subject: [PATCH 05/38] Update ESP32 workflows to currently supported ESP-IDF versions Removes the EOL ESP-IDF v4.x test and bumps verrsion 5.3 from the release candidate to the official v5.3 release. Signed-off-by: Winford --- .github/workflows/esp32-build.yaml | 9 +++------ .github/workflows/esp32-mkimage.yaml | 12 ++++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/esp32-build.yaml b/.github/workflows/esp32-build.yaml index eeff68cd7..1536cda20 100644 --- a/.github/workflows/esp32-build.yaml +++ b/.github/workflows/esp32-build.yaml @@ -38,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 83bb26f2c..d83d74307 100644 --- a/.github/workflows/esp32-mkimage.yaml +++ b/.github/workflows/esp32-mkimage.yaml @@ -32,18 +32,18 @@ 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"] env: @@ -51,7 +51,7 @@ jobs: CXX: ${{ matrix.cxx }} CFLAGS: ${{ matrix.cflags }} CXXFLAGS: ${{ matrix.cflags }} - ImageOS: "ubuntu20" + ImageOS: "ubuntu22" steps: - name: Checkout repo From a2f6953cf2819a3d5c8292e1386be72c49288dd8 Mon Sep 17 00:00:00 2001 From: Winford Date: Mon, 19 Aug 2024 13:21:24 -0700 Subject: [PATCH 06/38] Update release notes to match supportes esp-idf versions Bumps the links to current versions if esp-idf docs, adds v5.3 and removes EOL v4.4 from the supported versions table. Signed-off-by: Winford --- doc/release-notes.md.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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. From 1ce5620d0e301d24f4b772f692fb17b070a5e98e Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 14 Sep 2024 14:55:59 +0200 Subject: [PATCH 07/38] esp32-mkimage: release Elixir flavour with bigger boot.avm partition esp32boot.avm when including Elixir library doesn't fit into standard boot.avm partition. Starting from this, and Elixir flavour with a bigger boot.avm partition will be released. Signed-off-by: Davide Bettio --- .github/workflows/esp32-mkimage.yaml | 25 +++++++++++++++-------- CHANGELOG.md | 5 +++++ UPDATING.md | 6 ++++++ src/platforms/esp32/partitions-elixir.csv | 12 +++++++++++ 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 src/platforms/esp32/partitions-elixir.csv diff --git a/.github/workflows/esp32-mkimage.yaml b/.github/workflows/esp32-mkimage.yaml index e938ab58c..4a37946dd 100644 --- a/.github/workflows/esp32-mkimage.yaml +++ b/.github/workflows/esp32-mkimage.yaml @@ -43,6 +43,7 @@ jobs: elixir_version: ["1.17"] compiler_pkgs: ["clang-10"] soc: ["esp32", "esp32c2", "esp32c3", "esp32s2", "esp32s3", "esp32c6", "esp32h2"] + flavor: ["", "-elixir"] env: CC: ${{ matrix.cc }} @@ -117,27 +118,35 @@ 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 + 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 +154,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 +165,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/CHANGELOG.md b/CHANGELOG.md index 1c1b1793e..dd4d43b97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.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 + ## [0.6.4] - 2024-08-18 ### Added diff --git a/UPDATING.md b/UPDATING.md index c9a941c11..49a564a91 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -6,6 +6,12 @@ # AtomVM Update Instructions +## v0.6.4 -> v0.6.5 + +- 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/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 From 112945b53451b16fe2009f4800654ed0ffed4fb5 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 14 Sep 2024 14:57:34 +0200 Subject: [PATCH 08/38] ESP32: generate elixir_esp32boot.avm Do not include anymore Elixir by default into esp32boot.avm due to partition size constrains, introduce instead instead elixir_esp32boot.amv. Signed-off-by: Davide Bettio --- CHANGELOG.md | 9 +++++++++ UPDATING.md | 2 ++ libs/esp32boot/CMakeLists.txt | 6 +++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd4d43b97..fffd209ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ESP32: add a new Elixir release "flavor" with a bigger boot.avm partition that has room for Elixir standard library modules +### Changed + +- ESP32: Elixir library is not shipped anymore with `esp32boot.avm`. Use `elixir_esp32boot.avm` +instead + +### Fixed + +- ESP32: content of `boot.avm` partition is not truncated anymore + ## [0.6.4] - 2024-08-18 ### Added diff --git a/UPDATING.md b/UPDATING.md index 49a564a91..d914fe423 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -8,6 +8,8 @@ ## 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. 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) From 5fc54c90a11f58e21e5ff117a09b30534afdb068 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 14 Sep 2024 14:58:00 +0200 Subject: [PATCH 09/38] ESP32: update mkimage tool in order to support different boot.avm partitions Add support for `--boot` option so a different esp32boot.avm file (e.g. the one with Elixir support) can be used. Signed-off-by: Davide Bettio --- .github/workflows/esp32-mkimage.yaml | 8 ++++++-- CHANGELOG.md | 1 + src/platforms/esp32/tools/mkimage.config.in | 2 +- src/platforms/esp32/tools/mkimage.erl | 15 +++++++++++---- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/esp32-mkimage.yaml b/.github/workflows/esp32-mkimage.yaml index 4a37946dd..d5e155abe 100644 --- a/.github/workflows/esp32-mkimage.yaml +++ b/.github/workflows/esp32-mkimage.yaml @@ -135,9 +135,13 @@ jobs: - name: "Create a ${{ matrix.soc }}${{ matrix.flavor }} image" working-directory: ./src/platforms/esp32/build run: | - ./mkimage.sh - if [ ! -z "${{ matrix.flavor }}" ] + 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index fffd209ff..e12c7b87f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 ### Changed 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..4b10855c3 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,7 @@ 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 +206,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). From 0091bb8819ecfb0c92c440bd3beaa95bb5c60d24 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 14 Sep 2024 22:54:47 +0200 Subject: [PATCH 10/38] Add support to erlang:atom_to_binary/1 erlang:atom_to_binary/1 is just erlang:atom_to_binary(Atom, utf8). Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + src/libAtomVM/nifs.c | 10 ++++------ src/libAtomVM/nifs.gperf | 1 + 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e12c7b87f..ba12eea60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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)` ### Changed diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 0c1b1c6b0..047ef8599 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -94,7 +94,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[]); @@ -243,7 +243,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 = @@ -2072,14 +2072,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; diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index a39aa12bd..7e9bbbc0c 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 From 531588af154afa46d7c3b86d386cee9062d2d079 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 14 Sep 2024 22:54:51 +0200 Subject: [PATCH 11/38] Elixir library: add support for String.Chars protocol Code has been taken from here: https://raw.githubusercontent.com/elixir-lang/elixir/v1.17.2/lib/elixir/lib/string/chars.ex String.Chars.Float implementation has been slightly changed, since `:short` option is not yet supported, so `:compact` + `{:decimal, 17}` has been used. Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + libs/exavmlib/lib/CMakeLists.txt | 7 ++++ libs/exavmlib/lib/String.Chars.Atom.ex | 32 +++++++++++++++ libs/exavmlib/lib/String.Chars.BitString.ex | 35 ++++++++++++++++ libs/exavmlib/lib/String.Chars.Float.ex | 29 ++++++++++++++ libs/exavmlib/lib/String.Chars.Integer.ex | 28 +++++++++++++ libs/exavmlib/lib/String.Chars.List.ex | 26 ++++++++++++ libs/exavmlib/lib/String.Chars.ex | 44 +++++++++++++++++++++ tests/libs/exavmlib/Tests.ex | 13 +++++- 9 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 libs/exavmlib/lib/String.Chars.Atom.ex create mode 100644 libs/exavmlib/lib/String.Chars.BitString.ex create mode 100644 libs/exavmlib/lib/String.Chars.Float.ex create mode 100644 libs/exavmlib/lib/String.Chars.Integer.ex create mode 100644 libs/exavmlib/lib/String.Chars.List.ex create mode 100644 libs/exavmlib/lib/String.Chars.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index ba12eea60..1ee9af594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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 ### Changed diff --git a/libs/exavmlib/lib/CMakeLists.txt b/libs/exavmlib/lib/CMakeLists.txt index 92deabdb6..70749cfef 100644 --- a/libs/exavmlib/lib/CMakeLists.txt +++ b/libs/exavmlib/lib/CMakeLists.txt @@ -74,6 +74,13 @@ set(ELIXIR_MODULES Collectable.List Collectable.Map Collectable.MapSet + + 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/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/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 44ae8d2a1..47d9bcc15 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -19,13 +19,13 @@ # defmodule Tests do - @compile {:no_warn_undefined, :undef} def start() do :ok = IO.puts("Running Elixir tests") :ok = test_enum() :ok = test_exception() + :ok = test_chars_protocol() :ok = IO.puts("Finished Elixir tests") end @@ -150,6 +150,17 @@ 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 + defp fact(n) when n < 0, do: :test defp fact(0), do: 1 defp fact(n), do: fact(n - 1) * n From b5d9c8e8355fec44a4c11e7b47a901c61d3c59ee Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 15 Sep 2024 12:05:56 +0200 Subject: [PATCH 12/38] Elixir library: fix slice implementation for MapSet Use slice implementation from Elixir v1.10.4 instead of a newer one. Signed-off-by: Davide Bettio --- libs/exavmlib/lib/Enumerable.MapSet.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a92c1df979c71929282e96ce37cca7372f81848e Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 15 Sep 2024 12:06:05 +0200 Subject: [PATCH 13/38] Elixir library: add support for Enum.at/3 function Code has been taken from Elixir enum.ex. Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + libs/exavmlib/lib/Enum.ex | 34 ++++++++++++++++++++++++++++++++++ tests/libs/exavmlib/Tests.ex | 13 ++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ee9af594..1b84a196c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ 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 +- Support for Elixir `Enum.at/3` ### Changed diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index c702db55b..a09306460 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -27,6 +27,8 @@ defmodule Enum do @type index :: integer @type element :: any + @type default :: any + require Stream.Reducers, as: R defmacrop next(_, entry, acc) do @@ -61,6 +63,38 @@ defmodule Enum do any_list(enumerable, fun) 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. diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 47d9bcc15..45c36e94e 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -37,6 +37,7 @@ 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) # map 2 = Enum.count(%{a: 1, b: 2}) @@ -49,7 +50,12 @@ 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 # map set 3 = Enum.count(MapSet.new([0, 1, 2])) @@ -58,6 +64,10 @@ 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 # range 4 = Enum.count(1..4) @@ -66,6 +76,7 @@ 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) # into %{a: 1, b: 2} = Enum.into([a: 1, b: 2], %{}) From 664b2c12da657392fc0e775e936db751fcd196f8 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 15 Sep 2024 12:06:11 +0200 Subject: [PATCH 14/38] Elixir library: make Enum.find/3 work with Enumerable Code has been taken from Elixir enum.ex. Signed-off-by: Davide Bettio --- libs/exavmlib/lib/Enum.ex | 25 +++++++++++++++++++++++++ tests/libs/exavmlib/Tests.ex | 5 +++++ 2 files changed, 30 insertions(+) diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index a09306460..c9ad3f26a 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -128,10 +128,35 @@ defmodule Enum do filter_list(enumerable, fun) 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 + def find_index(enumerable, fun) when is_list(enumerable) do find_index_list(enumerable, 0, fun) end diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 45c36e94e..d54248d54 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -38,6 +38,7 @@ defmodule Tests do 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) # map 2 = Enum.count(%{a: 1, b: 2}) @@ -56,6 +57,8 @@ defmodule Tests do 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) # map set 3 = Enum.count(MapSet.new([0, 1, 2])) @@ -68,6 +71,7 @@ defmodule Tests do 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) # range 4 = Enum.count(1..4) @@ -77,6 +81,7 @@ defmodule Tests do 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) # into %{a: 1, b: 2} = Enum.into([a: 1, b: 2], %{}) From 1f9745606ac734588c3304f86fd7bec14fe3b38d Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 15 Sep 2024 13:52:02 +0200 Subject: [PATCH 15/38] Elixir library: add support to `String.Chars` protocol to `Enum.join` Now `Enum.join([1, 2, 3], ",")` is allowed. Signed-off-by: Davide Bettio --- CHANGELOG.md | 3 ++- libs/exavmlib/lib/Enum.ex | 11 +++++++++-- tests/libs/exavmlib/Tests.ex | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b84a196c..3b81b1f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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 +- 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` ### Changed diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index c9ad3f26a..8075002d6 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -448,12 +448,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 @@ -468,6 +468,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 @@ -669,6 +675,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/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index d54248d54..b5b367853 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -91,6 +91,8 @@ defmodule Tests do # 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]) From 7cc73a48b0c63c1636e6f43c40906bdb78dd30e6 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 15 Sep 2024 14:53:53 +0200 Subject: [PATCH 16/38] Elixir library: add support to Enumerable also for `Enum.all?` and `Enum.any?` Import code from Elixir v1.10.4. Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + libs/exavmlib/lib/Enum.ex | 86 ++++++++++++++++++++++++++++++++++++ tests/libs/exavmlib/Tests.ex | 8 ++++ 3 files changed, 95 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b81b1f63..056aa1801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Elixir standard library modules - 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 to Elixir `Enumerable` protocol also for `Enum.all?` and `Enum.any?` ### Changed diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index 8075002d6..076c56e39 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -55,14 +55,100 @@ 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). diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index b5b367853..699df4f84 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -39,6 +39,8 @@ defmodule Tests do [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) + true = Enum.all?([1, 2, 3], fn n -> n >= 0 end) + true = Enum.any?([1, -2, 3], fn n -> n < 0 end) # map 2 = Enum.count(%{a: 1, b: 2}) @@ -59,6 +61,8 @@ defmodule Tests do 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.all?(%{a: 1, b: 2}, fn {_k, v} -> v >= 0 end) + true = Enum.any?(%{a: 1, b: -2}, fn {_k, v} -> v < 0 end) # map set 3 = Enum.count(MapSet.new([0, 1, 2])) @@ -72,6 +76,8 @@ defmodule Tests do 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) + 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) # range 4 = Enum.count(1..4) @@ -82,6 +88,8 @@ defmodule Tests do [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.all?(0..10, fn n -> n >= 0 end) + true = Enum.any?(-1..10, fn n -> n < 0 end) # into %{a: 1, b: 2} = Enum.into([a: 1, b: 2], %{}) From b5e1f687c3d2e0b2b8f88e568eb67022d5bfeeb2 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 15 Sep 2024 15:15:19 +0200 Subject: [PATCH 17/38] Elixir library: add support to Enumerable also for `Enum.filter` Import code from Elixir v1.10.4. Signed-off-by: Davide Bettio --- CHANGELOG.md | 2 +- libs/exavmlib/lib/Enum.ex | 39 ++++++++++++++++++++++++++++++++++++ tests/libs/exavmlib/Tests.ex | 4 ++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 056aa1801..a51e35c0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Elixir standard library modules - 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 to Elixir `Enumerable` protocol also for `Enum.all?` and `Enum.any?` +- Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?` and `Enum.filter` ### Changed diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index 076c56e39..84b64e3a9 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -31,6 +31,10 @@ defmodule Enum do require Stream.Reducers, as: R + defmacrop skip(acc) do + acc + end + defmacrop next(_, entry, acc) do quote(do: [unquote(entry) | unquote(acc)]) end @@ -210,10 +214,45 @@ defmodule Enum do :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`. diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 699df4f84..868767f83 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -41,6 +41,7 @@ defmodule Tests do :atom = Enum.find([1, 2, :atom, 3, 4], -1, fn item -> not is_integer(item) 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) # map 2 = Enum.count(%{a: 1, b: 2}) @@ -63,6 +64,7 @@ defmodule Tests do {:d, 3} = Enum.find(%{a: 1, b: 2, c: :atom, d: 3}, fn {k, _v} -> k == :d 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) # map set 3 = Enum.count(MapSet.new([0, 1, 2])) @@ -78,6 +80,7 @@ defmodule Tests do :atom = Enum.find(MapSet.new([1, 2, :atom, 3, 4]), fn item -> not is_integer(item) 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) # range 4 = Enum.count(1..4) @@ -90,6 +93,7 @@ defmodule Tests do 8 = Enum.find(-10..10, fn item -> item >= 8 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) # into %{a: 1, b: 2} = Enum.into([a: 1, b: 2], %{}) From e8d214b58618b77cd9942d273d7c396eb987fd1a Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 15 Sep 2024 15:27:17 +0200 Subject: [PATCH 18/38] Elixir library: add support to Enumerable also for `Enum.each` Import code from Elixir v1.10.4. Signed-off-by: Davide Bettio --- CHANGELOG.md | 3 ++- libs/exavmlib/lib/Enum.ex | 23 +++++++++++++++++++++++ tests/libs/exavmlib/Tests.ex | 4 ++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a51e35c0c..b1b430b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ Elixir standard library modules - 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 to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?` and `Enum.filter` +- Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each` and +`Enum.filter` ### Changed diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index 84b64e3a9..4c8401e13 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -209,11 +209,34 @@ defmodule Enum do end 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. diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 868767f83..24df99952 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -42,6 +42,7 @@ defmodule Tests do 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) + :ok = Enum.each([1, 2, 3], fn n -> true = is_integer(n) end) # map 2 = Enum.count(%{a: 1, b: 2}) @@ -65,6 +66,7 @@ defmodule Tests do 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) + :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])) @@ -81,6 +83,7 @@ defmodule Tests do 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) + :ok = Enum.each(MapSet.new([1, 2, 3]), fn n -> true = is_integer(n) end) # range 4 = Enum.count(1..4) @@ -94,6 +97,7 @@ defmodule Tests do 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) + :ok = Enum.each(-5..5, fn n -> true = is_integer(n) end) # into %{a: 1, b: 2} = Enum.into([a: 1, b: 2], %{}) From 67a2da56fbd79fb8b3a40bd76185735e3320849f Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 15 Sep 2024 18:26:16 +0200 Subject: [PATCH 19/38] Format esp32/tools/mkimage.erl code Just run rebar3 fmt on it. Signed-off-by: Davide Bettio --- src/platforms/esp32/tools/mkimage.erl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/platforms/esp32/tools/mkimage.erl b/src/platforms/esp32/tools/mkimage.erl index 4b10855c3..fd9d73ed2 100644 --- a/src/platforms/esp32/tools/mkimage.erl +++ b/src/platforms/esp32/tools/mkimage.erl @@ -162,7 +162,13 @@ mkimage(RootDir, BuildDir, BootFile, OutputFile, Segments) -> end end, SegmentPaths = [ - replace("BUILD_DIR", BuildDir, replace("BOOT_FILE", BootFile, 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 From baaea8c3ece855b27df7de8a6493113158db6b6d Mon Sep 17 00:00:00 2001 From: Peter M Date: Thu, 25 Jul 2024 22:00:14 +0200 Subject: [PATCH 20/38] Support handle_continue in gen_server.erl Adds GenServer support for handle_continue (https://www.erlang.org/doc/apps/stdlib/gen_server.html#c:handle_continue/2), tests are carbon copy from upstream test suite https://github.com/erlang/otp/blob/141120ab9de7e6069ee45280dc7f6a251f89e081/lib/stdlib/test/gen_server_SUITE.erl#L1012 Added to otp back in 2017: https://github.com/erlang/otp/pull/1490 - often used in elixir land. Signed-off-by: Peter M --- CHANGELOG.md | 1 + libs/estdlib/src/gen_server.erl | 44 +++++++++++-- tests/libs/estdlib/test_gen_server.erl | 85 +++++++++++++++++++++++++- 3 files changed, 124 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b430b39..6e104b46a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")` - Support for Elixir `Enum.at/3` - Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each` and `Enum.filter` +- Add support for `handle_continue` callback in `gen_server` ### Changed diff --git a/libs/estdlib/src/gen_server.erl b/libs/estdlib/src/gen_server.erl index 022f86154..f8de330d0 100644 --- a/libs/estdlib/src/gen_server.erl +++ b/libs/estdlib/src/gen_server.erl @@ -68,29 +68,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) -> @@ -154,6 +161,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 +201,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. @@ -434,6 +452,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 +468,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 +492,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/tests/libs/estdlib/test_gen_server.erl b/tests/libs/estdlib/test_gen_server.erl index bf88d1b16..dd216b509 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,7 @@ test() -> ok = test_cast(), ok = test_info(), ok = test_start_link(), + ok = test_continue(), ok = test_init_exception(), ok = test_late_reply(), ok = test_concurrent_clients(), @@ -77,6 +78,50 @@ test_start_link() -> true = erlang:process_flag(trap_exit, false), ok. +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, [], []), @@ -353,11 +398,35 @@ test_stop_noproc() -> 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 +461,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 +475,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 From d3ec5c081b7e43a5ee1ef43024a831a52c5e0b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Tr=C3=B3jniak?= Date: Tue, 3 Sep 2024 20:08:41 +0200 Subject: [PATCH 21/38] fix: ESP32 gpiodriver interrupt pin number handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cast Pin number to integer after checking if it is integer. Now set_int can set interrupt on any pin, not only pin 2. Improved pin_int typing after casting to int Signed-off-by: Rafał Trójniak --- CHANGELOG.md | 1 + src/platforms/esp32/components/avm_builtins/gpio_driver.c | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b430b39..399f4fef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ instead ### Fixed - ESP32: content of `boot.avm` partition is not truncated anymore +- ESP32: Fixed gpio:set_int` to accept any pin, not only pin 2 ## [0.6.4] - 2024-08-18 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; } From 1603fd5c333ad515caae11531828ce2ec7799301 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sun, 22 Sep 2024 15:41:15 +0200 Subject: [PATCH 22/38] Add support for `is_bistring/1` construct Fix implementation of OP_IS_BITSTR to return true for binaries Add erlang:is_bitstring/1 bif (actually implemented by erlang:is_binary/1) for OTP 21 and 22. `is_bitstring/1` construct is used by Elixir protocols such as List.Chars. Signed-off-by: Paul Guyot --- CHANGELOG.md | 1 + src/libAtomVM/bifs.gperf | 1 + src/libAtomVM/opcodesswitch.h | 4 +- tests/erlang_tests/CMakeLists.txt | 2 + .../test_is_bitstring_is_binary.erl | 50 +++++++++++++++++++ tests/test.c | 1 + 6 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/erlang_tests/test_is_bitstring_is_binary.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index 399f4fef2..08b29515b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")` - Support for Elixir `Enum.at/3` - Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each` and `Enum.filter` +- Add support for `is_bitstring/1` construct which is used in Elixir protocols runtime. ### Changed 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/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 3fd95da72..896114868 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -5363,7 +5363,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/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index bc01b4b38..b323448d3 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) @@ -607,6 +608,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 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/test.c b/tests/test.c index 878bbce53..70e0f36d1 100644 --- a/tests/test.c +++ b/tests/test.c @@ -166,6 +166,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), From c8d66336af75b04b3f0dd2f256be206be8a04365 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 21 Sep 2024 22:22:34 +0200 Subject: [PATCH 23/38] Elixir library: add `Enum.chunk_by` and `Enum.chunk_while` Code has been taken from here: https://raw.githubusercontent.com/elixir-lang/elixir/v1.10.4/lib/elixir/lib/enum.ex Signed-off-by: Davide Bettio --- CHANGELOG.md | 4 +- libs/exavmlib/lib/Enum.ex | 93 ++++++++++++++++++++++++++++++++++++ tests/libs/exavmlib/Tests.ex | 25 ++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b29515b..ecccafd9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,9 @@ Elixir standard library modules - 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 to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each` and -`Enum.filter` - 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.chunk_by` and `Enum.chunk_while` ### Changed diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index 4c8401e13..f830e44f6 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -24,6 +24,7 @@ defmodule Enum do @compile {:autoload, false} @type t :: Enumerable.t() + @type acc :: any @type index :: integer @type element :: any @@ -209,6 +210,98 @@ 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`. diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 24df99952..26a7b4714 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -113,6 +113,9 @@ defmodule Tests do # 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) @@ -132,6 +135,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 From a244850bec7c8795e3a672c8a51a1923d2937d11 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 21 Sep 2024 22:36:32 +0200 Subject: [PATCH 24/38] Elixir library: add `Enum.reject` Code has been taken from here: https://raw.githubusercontent.com/elixir-lang/elixir/v1.10.4/lib/elixir/lib/enum.ex Signed-off-by: Davide Bettio --- CHANGELOG.md | 2 +- libs/exavmlib/lib/Enum.ex | 25 +++++++++++++++++++++---- tests/libs/exavmlib/Tests.ex | 4 ++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecccafd9d..d8c9ec199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ 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.chunk_by` and `Enum.chunk_while` +`Enum.filter`, `Enum.reject`, `Enum.chunk_by` and `Enum.chunk_while` ### Changed diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index f830e44f6..666474f45 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -508,10 +508,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 @@ -743,6 +739,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. diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 26a7b4714..9c8e96687 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -42,6 +42,7 @@ defmodule Tests do 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 @@ -66,6 +67,7 @@ defmodule Tests do 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 @@ -83,6 +85,7 @@ defmodule Tests do 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 @@ -97,6 +100,7 @@ defmodule Tests do 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 From 539ffd5e0d4fe2a5fb0d6694c333ba205432d1db Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 23 Sep 2024 00:25:22 +0200 Subject: [PATCH 25/38] Elixir library: add `Enum.flat_map` Code has been taken from here: https://raw.githubusercontent.com/elixir-lang/elixir/v1.10.4/lib/elixir/lib/enum.ex Signed-off-by: Davide Bettio --- CHANGELOG.md | 2 +- libs/exavmlib/lib/Enum.ex | 47 ++++++++++++++++++++++++++++++++++++ tests/libs/exavmlib/Tests.ex | 5 ++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8c9ec199..24e7ae361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ 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.reject`, `Enum.chunk_by` and `Enum.chunk_while` +`Enum.filter`, `Enum.flat_map`, `Enum.reject`, `Enum.chunk_by` and `Enum.chunk_while` ### Changed diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index 666474f45..f2fa654f7 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -406,6 +406,40 @@ defmodule Enum do find_value_list(enumerable, default, fun) 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`. @@ -588,6 +622,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`. diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 9c8e96687..69406e1b1 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -109,6 +109,11 @@ 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], ", ") From f9e3084aa62c6a2f0ac3daa09f8807a196454593 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 23 Sep 2024 19:50:29 +0200 Subject: [PATCH 26/38] Fix memory corruption in unicode:characters_to_binary `term_binary_data_size_in_terms` is dangerous since it doesn't account for binary header size, so a memory corruption is almost certain. Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + src/libAtomVM/nifs.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b29515b..a3e23a6da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ instead - 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` ## [0.6.4] - 2024-08-18 diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 047ef8599..3febecd05 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -4616,7 +4616,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; } From fbd2f7480d605418be8fec034db9ba2811301fa3 Mon Sep 17 00:00:00 2001 From: Peter M Date: Sun, 22 Sep 2024 21:31:11 +0200 Subject: [PATCH 27/38] Process.ex send/2 send_after/3 and /4 cancel_timer/1 send_after is stubbed for /4 and abs option is ignored. Believe those are related to time warping. Signed-off-by: Peter M --- CHANGELOG.md | 1 + libs/exavmlib/lib/Process.ex | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b29515b..44cdff3e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")` - Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each` and `Enum.filter` - Add support for `is_bitstring/1` construct which is used in Elixir protocols runtime. +- Add support to Elixir for `Process.send/2` `Process.send_after/3/4` and `Process.cancel_timer/1` ### Changed 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 From 6b5da55c95f7ee017c87dd4f714e4e229ac5a57d Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 23 Sep 2024 00:30:07 +0200 Subject: [PATCH 28/38] Elixir library: add support to Enumerable to `Enum.find_index`/`find_value` Code has been taken from here: https://raw.githubusercontent.com/elixir-lang/elixir/v1.10.4/lib/elixir/lib/enum.ex Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + libs/exavmlib/lib/Enum.ex | 53 ++++++++++++++++++++++++++++++++++++ tests/libs/exavmlib/Tests.ex | 5 ++++ 3 files changed, 59 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24e7ae361..2bf6a464a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")` - 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 diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index f2fa654f7..a36d9f1a9 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -398,14 +398,67 @@ defmodule Enum do |> 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. diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 69406e1b1..98e4f7bd9 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -39,6 +39,8 @@ defmodule Tests do [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) @@ -64,6 +66,7 @@ defmodule Tests do 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) @@ -82,6 +85,7 @@ defmodule Tests do 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) @@ -97,6 +101,7 @@ defmodule Tests do [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) From 9aae34493d05bb394394722e3393bb865ffaa412 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 21 Sep 2024 23:15:53 +0200 Subject: [PATCH 29/38] Fix decoding of large index of literals (fix #1272) Signed-off-by: Paul Guyot --- CHANGELOG.md | 3 +- src/libAtomVM/opcodesswitch.h | 26 ++++++++--- tests/erlang_tests/CMakeLists.txt | 2 + tests/erlang_tests/literal_test2.erl | 67 ++++++++++++++++++++++++++++ tests/test.c | 1 + 5 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 tests/erlang_tests/literal_test2.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e23a6da..49addd7a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,9 @@ instead ### Fixed - ESP32: content of `boot.avm` partition is not truncated anymore -- ESP32: Fixed gpio:set_int` to accept any pin, not only pin 2 +- 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 ## [0.6.4] - 2024-08-18 diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 896114868..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; \ \ diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index b323448d3..39597e965 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -321,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) @@ -788,6 +789,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/test.c b/tests/test.c index 70e0f36d1..0ed33d2cc 100644 --- a/tests/test.c +++ b/tests/test.c @@ -336,6 +336,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), From ab4272b9edd4dd58029ff1532dc6431cabd64233 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sun, 22 Sep 2024 17:30:34 +0200 Subject: [PATCH 30/38] Add support for `maps:merge_with/3` Signed-off-by: Paul Guyot --- CHANGELOG.md | 1 + libs/estdlib/src/maps.erl | 36 +++++++++++++++++++++++ tests/libs/estdlib/test_maps.erl | 50 ++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e23a6da..d0f63f3f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")` - Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each` and `Enum.filter` - Add support for `is_bitstring/1` construct which is used in Elixir protocols runtime. +- Support for `maps:merge_with/3` ### Changed 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/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}), From a31dcbfa7d6f6aecf62bf4e9b388515c20a916d0 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sun, 22 Sep 2024 16:05:46 +0200 Subject: [PATCH 31/38] Add `lists:last/1` and `lists:mapfoldl/3` Signed-off-by: Paul Guyot --- CHANGELOG.md | 1 + libs/estdlib/src/lists.erl | 30 ++++++++++++++++++++++++++++++ tests/libs/estdlib/test_lists.erl | 15 +++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e23a6da..dbbb1e6d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")` - Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each` and `Enum.filter` - Add support for `is_bitstring/1` construct which is used in Elixir protocols runtime. +- Support for `lists:last/1` and `lists:mapfoldl/3` ### Changed 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/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. From abe689aa8e207ab8f1c36ab87ca1ad4e474f066f Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 22 Sep 2024 22:22:13 +0200 Subject: [PATCH 32/38] Fix bogus out_of_memory in `unicode:characters_to_list` Quoting `man malloc`: "If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free()." Platforms such as esp-idf prefers NULL in that case. So `IS_NULL_PTR` may fail in situations such as when dealing with `malloc(size)` when size is 0 (due to a 0 length binary, leading to out of memory when doing `unicode:characters_to_list(<<>>)` on ESP32 but not on other platforms. Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + src/libAtomVM/nifs.c | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49addd7a9..62cdc260a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ instead - 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 ## [0.6.4] - 2024-08-18 diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 3febecd05..c25f45736 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -4552,7 +4552,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; From 7f728853087d7ed41798d2fd38e9bfafcce7ce03 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 22 Sep 2024 23:36:34 +0200 Subject: [PATCH 33/38] tests: add build time option for emulating malloc(0) behavior Allow to override malloc/calloc functions to provide a version that behaves like esp-idf one, when malloc(0) is called. Signed-off-by: Davide Bettio --- tests/test.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test.c b/tests/test.c index 0ed33d2cc..f7a7f699a 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), From 7faa258df540362cb8ec7b1e4d6332c2551fd2b9 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sun, 22 Sep 2024 16:07:23 +0200 Subject: [PATCH 34/38] Add Elixir List.Chars protocol. https://github.com/elixir-lang/elixir/blob/v1.17.2/lib/elixir/lib/list/chars.ex Split into several source files because of how our CMakeLists.txt is built Signed-off-by: Paul Guyot --- CHANGELOG.md | 1 + libs/exavmlib/lib/CMakeLists.txt | 7 ++++ libs/exavmlib/lib/List.Chars.Atom.ex | 26 +++++++++++++++ libs/exavmlib/lib/List.Chars.BitString.ex | 36 +++++++++++++++++++++ libs/exavmlib/lib/List.Chars.Float.ex | 27 ++++++++++++++++ libs/exavmlib/lib/List.Chars.Integer.ex | 26 +++++++++++++++ libs/exavmlib/lib/List.Chars.List.ex | 25 +++++++++++++++ libs/exavmlib/lib/List.Chars.ex | 39 +++++++++++++++++++++++ 8 files changed, 187 insertions(+) create mode 100644 libs/exavmlib/lib/List.Chars.Atom.ex create mode 100644 libs/exavmlib/lib/List.Chars.BitString.ex create mode 100644 libs/exavmlib/lib/List.Chars.Float.ex create mode 100644 libs/exavmlib/lib/List.Chars.Integer.ex create mode 100644 libs/exavmlib/lib/List.Chars.List.ex create mode 100644 libs/exavmlib/lib/List.Chars.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 97522e883..da99ca8d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")` - 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 ### Changed diff --git a/libs/exavmlib/lib/CMakeLists.txt b/libs/exavmlib/lib/CMakeLists.txt index 70749cfef..c55498e80 100644 --- a/libs/exavmlib/lib/CMakeLists.txt +++ b/libs/exavmlib/lib/CMakeLists.txt @@ -75,6 +75,13 @@ set(ELIXIR_MODULES 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 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 From 03ee7640ceacd545ff255f1036ea7d5457438d31 Mon Sep 17 00:00:00 2001 From: Peter M Date: Sun, 22 Sep 2024 21:51:43 +0200 Subject: [PATCH 35/38] System.ex monotonic_time/1 system_time/1 Carbon copy from elixir with :native time unit removed, as it's not supported by current erlang implementation. Signed-off-by: Peter M --- libs/exavmlib/lib/CMakeLists.txt | 1 + libs/exavmlib/lib/System.ex | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 libs/exavmlib/lib/System.ex diff --git a/libs/exavmlib/lib/CMakeLists.txt b/libs/exavmlib/lib/CMakeLists.txt index 70749cfef..80c33f9b0 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 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 From 11fbf918c2ce82b3e6f7613865d3711ab2f40c95 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Tue, 24 Sep 2024 20:29:21 +0200 Subject: [PATCH 36/38] Add support for `gen_server:start_monitor/3,4` Similar to start_link but with `monitor` option. Code for all start with name functions has been refactored to avoid duplication. Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + libs/estdlib/src/gen_server.erl | 79 ++++++++++++++++++++------ tests/libs/estdlib/test_gen_server.erl | 27 +++++++++ 3 files changed, 91 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da99ca8d2..9f84fc057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ also non string parameters (e.g. `Enum.join([1, 2], ",")` - 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 diff --git a/libs/estdlib/src/gen_server.erl b/libs/estdlib/src/gen_server.erl index f8de330d0..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, @@ -109,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. @@ -123,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 -> @@ -209,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. @@ -246,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 @@ -292,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 @@ -316,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. diff --git a/tests/libs/estdlib/test_gen_server.erl b/tests/libs/estdlib/test_gen_server.erl index dd216b509..14a6396af 100644 --- a/tests/libs/estdlib/test_gen_server.erl +++ b/tests/libs/estdlib/test_gen_server.erl @@ -36,6 +36,7 @@ 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(), @@ -78,6 +79,24 @@ 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), @@ -392,6 +411,14 @@ 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 %% From 5caab38da80c7ef83187583058622745faf03981 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 25 Sep 2024 00:29:29 +0200 Subject: [PATCH 37/38] Elixir library: fix crash with `inspect(:atom)` inspect was returning a list instead of a binary. Also fix handling of atom special cases. Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + libs/exavmlib/lib/Kernel.ex | 26 +++++++++++++++++++++----- tests/libs/exavmlib/Tests.ex | 13 +++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f84fc057..59f02c9fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ instead - 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)` ## [0.6.4] - 2024-08-18 diff --git a/libs/exavmlib/lib/Kernel.ex b/libs/exavmlib/lib/Kernel.ex index 4d5088878..b7b241875 100644 --- a/libs/exavmlib/lib/Kernel.ex +++ b/libs/exavmlib/lib/Kernel.ex @@ -42,7 +42,7 @@ 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) @@ -118,10 +118,26 @@ 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 @doc """ diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 98e4f7bd9..782da05d1 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -26,6 +26,7 @@ defmodule Tests do :ok = test_enum() :ok = test_exception() :ok = test_chars_protocol() + :ok = test_inspect() :ok = IO.puts("Finished Elixir tests") end @@ -234,6 +235,18 @@ defmodule Tests do :ok end + def test_inspect() do + "true" = inspect(true) + "false" = inspect(false) + "nil" = inspect(nil) + + ":test" = inspect(:test) + ":アトム" = inspect(:アトム) + "Test" = inspect(Test) + + :ok + end + defp fact(n) when n < 0, do: :test defp fact(0), do: 1 defp fact(n), do: fact(n - 1) * n From 36cb1b84bd75088a67284ac6202f2db874158af9 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 25 Sep 2024 01:34:34 +0200 Subject: [PATCH 38/38] Elixir library: make inspect() more similar to Elixir one inspect() now supports lists, improper lists, it adds quotes to printable lists, etc... Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + libs/exavmlib/lib/Kernel.ex | 51 ++++++++++++++++++++++++++++++++---- tests/libs/exavmlib/Tests.ex | 48 +++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f02c9fc..3dce17c03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ instead - 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 diff --git a/libs/exavmlib/lib/Kernel.ex b/libs/exavmlib/lib/Kernel.ex index b7b241875..491212d87 100644 --- a/libs/exavmlib/lib/Kernel.ex +++ b/libs/exavmlib/lib/Kernel.ex @@ -48,8 +48,13 @@ defmodule Kernel do :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 @@ -140,6 +153,34 @@ defmodule Kernel do 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/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 782da05d1..fe34c30e8 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -19,6 +19,11 @@ # defmodule Tests do + # defstruct [ + # :field1, + # field2: 42 + # ] + @compile {:no_warn_undefined, :undef} def start() do @@ -244,10 +249,53 @@ defmodule Tests do ":アトム" = 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