├── .github └── workflows │ └── erlang.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── build-aux └── google2792824a0f71459b.html ├── rebar.config ├── src ├── defarg.erl ├── erlpipe.erl ├── etran.app.src ├── etran.erl ├── etran_util.erl ├── iif.erl ├── listcomp.erl └── str.erl └── test ├── defarg_test.erl ├── erlpipe_test.erl ├── etran_test.erl ├── iif_test.erl ├── listcomp_test.erl └── str_test.erl /.github/workflows/erlang.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | container: 16 | image: erlang:24.0 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Install gawk 21 | run: apt update && apt install sudo && sudo apt-get install gawk 22 | - name: Compile 23 | run: make 24 | - name: Run tests 25 | run: make test 26 | #- name: Make documentation 27 | # run: make gh-pages 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | *.o 3 | *.beam 4 | *.plt 5 | erl_crash.dump 6 | .concrete/DEV_MODE 7 | 8 | # rebar 2.x 9 | .rebar 10 | rebar.lock 11 | rel/example_project 12 | ebin 13 | deps 14 | doc 15 | build-aux 16 | 17 | # rebar 3 18 | .rebar3 19 | _build/ 20 | _checkouts/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Serge Aleynikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT := $(notdir $(PWD)) 2 | 3 | all: compile 4 | 5 | compile: 6 | rebar3 $@ 7 | 8 | clean: 9 | rm -fr doc ebin _build build-aux/*.{edoc,awk,css,sh} 10 | 11 | distclean: clean 12 | rm -f build-aux/*.mk 13 | 14 | docs doc: 15 | rebar3 ex_doc 16 | 17 | test: 18 | ERL_LIBS= rebar3 eunit 19 | 20 | publish: 21 | rebar3 hex $(if $(replace),publish --replace,cut) 22 | 23 | info:: 24 | @echo "make [compile] - Compile $(PROJECT)" 25 | @echo "make clean - Clean all build artifacts of $(PROJECT)" 26 | @echo "make distclean - Clean all build artifacts of $(PROJECT) and build-aux/" 27 | @echo "make publish [replace=1] - Publish/replace the library to hex.pm" 28 | @echo "make debug module=Mod file=File - Debug application of the Mod parse transform on the File" 29 | @echo "make debug-ui module=Mod file=File - Start a UI debugger for the Mod parse transform on the File" 30 | 31 | debug: 32 | @[ -z "$(module)" -o -z "$(file)" ] && echo "Run 'make $@ module=[erlpipe] file=FileName[.erl] [print=1]'" && exit 1 || true 33 | erlc +debug_info -pa _build/default/lib/etran/ebin -o _build/default/lib/etran/ebin src/$(module).erl 34 | erlc +debug_info $(DEBUG) +'{parse_transform,$(module)}' -pa _build/default/lib/etran/ebin \ 35 | $(if $(print),-D$(module)_orig -D$(module)_ast -D$(module)_src) -o _build/default/lib/etran/ebin $(basename $(file)).erl 36 | @cd _build/default/lib/etran/ebin && erl -pa . -eval 'decompiler:run("$(basename $(file)).beam"), halt(0).' -noinput 37 | @echo "====================================" 38 | @echo "Source:" && cat $(basename $(file)).erl 39 | @echo 40 | @echo "====================================" 41 | @echo "Result:" && cat _build/default/lib/etran/ebin/$(basename $(file)).erl 42 | @erl -pa _build/default/lib/etran/ebin -eval 'case proplists:get_value($(if $(fun),$(fun),test), $(basename $(file)):module_info(exports), -1) of -1 -> ok; _ -> io:format("\n\nOutput:\n ~p\n", [$(basename $(file)):$(if $(fun),$(fun),test)()]) end, halt(0).' -noinput 43 | @rm -f _build/default/lib/etran/ebin/$(basename $(file)).{erl,beam} 44 | 45 | debug-ui: DBG=debugger:start(), i:iaa([break]), i:ii($(module)), i:ib($(module),parse_transform,2) 46 | debug-ui: COMPILE=compile:file("$(basename $(file)).erl", [{d,$(module)_orig},{d,$(module)_ast},{d,$(module)_src},debug_info,{parse_transform,$(module)}]) 47 | debug-ui: 48 | [ -z "$(module)" -o -z "$(file)" ] && echo "Run 'make $@ module=[erlpipe] file=FileName[.erl]'" && exit 1 || true 49 | erl -pa _build/default/lib/etran/ebin -eval '$(DBG), $(COMPILE).' 50 | 51 | .PHONY: test doc 52 | .SUFFIXES: 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Collection of Erlang Parse Transforms 2 | 3 | **Author**: Serge Aleynikov 4 | 5 | **License**: MIT License 6 | 7 | [![build](https://github.com/saleyn/etran/actions/workflows/erlang.yml/badge.svg)](https://github.com/saleyn/etran/actions/workflows/erlang.yml) 8 | [![Hex.pm](https://img.shields.io/hexpm/v/etran.svg)](https://hex.pm/packages/etran) 9 | [![Hex.pm](https://img.shields.io/hexpm/dt/etran.svg)](https://hex.pm/packages/etran) 10 | 11 | This library includes useful parse transforms including Elixir-like pipeline operator for 12 | cascading function calls. 13 | 14 | ## Content 15 | 16 | | Module | Description | 17 | | ----------- | ------------------------------------------------------------------------------------ | 18 | | `defarg` | Support default argument values in Erlang functions | 19 | | `erlpipe` | Elixir-like pipe operator for Erlang | 20 | | `listcomp` | Fold Comprehension and Indexed List Comprehension | 21 | | `iif` | Ternary if function including `iif/3`, `iif/4`, `nvl/2`, `nvl/3` parse transforms | 22 | | `str` | Stringification functions including `str/1`, `str/2`, and `throw/2` parse transforms | 23 | 24 | ## `defarg`: Support default argument values in Erlang functions 25 | 26 | Presently the Erlang syntax doesn't allow function arguments to have default 27 | parameters. Consequently a developer needs to replicate the function 28 | definition multiple times passing constant defaults to some parameters of 29 | functions. 30 | 31 | This parse transform addresses this shortcoming by extending the syntax 32 | of function definitions at the top level in a module to have a default 33 | expression such that for `A / Default` argument the `Default` will be 34 | used if the function is called in code without that argument. 35 | 36 | Though it might seem more intuitive for programmers coming from other 37 | languages to use the assignment operator `=` for defining default arguments, 38 | using that operator would change the current meaning of pattern matching of 39 | arguments in function calls (i.e. `test(A=10)` is presently a valid expression). 40 | Therefore we chose the `/` operator for declaring default arguments because 41 | it has no valid meaning when applied in declaration of function arguments, 42 | and presently without the `defarg` transform, using this operator 43 | (e.g. `test(A / 10) -> ...`) would result in a syntax error detected by the 44 | compiler. 45 | 46 | ```erlang 47 | -export([t/2]). 48 | 49 | test(A / 10, B / 20) -> 50 | A + B. 51 | ``` 52 | The code above is transformed to: 53 | ```erlang 54 | -export([t/2]). 55 | -export([t/0, t/1]). 56 | 57 | test() -> test(10); 58 | test(A) -> test(A, 20); 59 | test(A,B) -> A+B. 60 | ``` 61 | 62 | The arguments with default values must be at the end of the argument list: 63 | ```erlang 64 | test(A, B, C / 1) -> %% This is valid 65 | ... 66 | 67 | test(A / 1, B, C) -> %% This is invalid 68 | ... 69 | ``` 70 | 71 | NOTE: The default arguments should be constant expressions. Function calls in default 72 | arguments are not supported! 73 | ```erlang 74 | test(A / erlang:timestamp()) -> %% !!! Bad syntax 75 | ... 76 | ``` 77 | 78 | ## `erlpipe`: Erlang Pipe Operator 79 | 80 | Inspired by the Elixir's `|>` pipeline operator. 81 | This transform makes code with cascading function calls much more readable by using the `/` as the 82 | pipeline operator. In the `LHS / RHS / ... Last.` notation, the result of evaluation of the LHS 83 | expression is passed as an argument to the RHS expression. This process continues until the `Last` 84 | expression is evaluated. The head element of the pipeline must be either a term to which the 85 | arithmetic division `/` operator cannot apply (i.e. not integers, floats, variables, functions), 86 | or if you need to pass an integer, float, variable, or a result of a function call, wrap it in a 87 | list brackets. 88 | 89 | It transforms code from: 90 | 91 | ```erlang 92 | print(L) when is_list(L) -> 93 | [3, L] %% Multiple items in a list become arguments to the first function 94 | / lists:split %% In Module:Function calls parenthesis are optional 95 | / element(1, _) %% '_' is the placeholder for the return value of a previous call 96 | / binary_to_list 97 | / io:format("~s\n", [_]). 98 | 99 | test1(Arg1, Arg2, Arg3) -> 100 | [Arg1, Arg2] %% Arguments must be enclosed in `[...]` 101 | / fun1 %% In function calls parenthesis are optional 102 | / mod:fun2 103 | / fun3() 104 | / fun4(Arg3, _) %% '_' is the placeholder for the return value of a previous call 105 | / fun ff/1 %% Inplace function references are supported 106 | / fun erlang:length/1 %% Inplace Mod:Fun/Arity function references are supported 107 | / fun(I) -> I end %% This lambda will be evaluated as: (fun(I) -> I end)(_) 108 | / io_lib:format("~p\n", [_]) 109 | / fun6([1,2,3], _, other_param) 110 | / fun7. 111 | 112 | test2() -> 113 | % Result = Argument / Function 114 | 3 = abc / atom_to_list / length, %% Atoms can be passed to '/' as is 115 | 3 = "abc" / length, %% Strings can be passed to '/' as is 116 | "abc" = <<"abc">> / binary_to_list, %% Binaries can be passed to '/' as is 117 | 118 | "1,2,3" = {$1,$2,$3} / tuple_to_list %% Tuples can be passed to '/' as is 119 | / [[I] || I <- _] %% The '_' placeholder is replaced by the return of tuple_to_list/1 120 | / string:join(","), %% Here a call to string:join/2 is made 121 | 122 | "1" = [min(1,2)] / integer_to_list, %% Function calls, integer and float value 123 | "1" = [1] / integer_to_list, %% arguments must be enclosed in a list. 124 | "1.0" = [1.0] / float_to_list([{decimals,1}]), 125 | "abc\n" = "abc" / (_ ++ "\n"), %% Can use operators on the right hand side 126 | 2.0 = 4.0 / max(1.0, 2.0), %% Expressions with lhs floats are unmodified 127 | 2 = 4 / max(1, 2). %% Expressions with lhs integers are unmodified 128 | 129 | test3() -> 130 | A = 10, 131 | B = 5, 132 | 2 = A / B, %% LHS variables (e.g. A) are not affected by the transform 133 | 2.0 = 10 / 5, %% Arithmetic division for integers, floats, variables is unmodified 134 | 2.0 = A / 5, %% (ditto) 135 | 5 = max(A,B) / 2. %% Use of division on LHS function calls is unaffected by the transform 136 | ``` 137 | 138 | to the following equivalent: 139 | 140 | ```erlang 141 | test1(Arg1, Arg2, Arg3) -> 142 | fun7(fun6([1,2,3], 143 | io_lib:format("~p\n", [ 144 | (fun(I) -> I end)( 145 | erlang:length( 146 | ff(fun4(Arg3, fun3(mod2:fun2(fun1(Arg1, Arg2)))))))]), 147 | other_param)). 148 | 149 | print(L) when is_list(L) -> 150 | io:format("~s\n", [binary_to_list(element(1, lists:split(3, L)))]). 151 | 152 | test2() -> 153 | 3 = length(atom_to_list(abc)), 154 | 3 = length("abc"), 155 | "abc" = binary_to_list(<<"abc">>), 156 | "1,2,3" = string:join([[I] || I <- tuple_to_list({$1,$2,$3})], ","), 157 | "1" = integer_to_list(min(1,2)), 158 | "1" = integer_to_list(1), 159 | "1.0" = float_to_list(1.0, [{decimals,1}]), 160 | "abc\n" = "abc" ++ "\n", 161 | 2.0 = 4.0 / max(1.0, 2.0), 162 | 2 = 4 / max(1, 2). 163 | ``` 164 | 165 | Similarly to Elixir, a special `tap/2` function is implemented, which 166 | passes the given argument to an anonymous function, returning the argument 167 | itself. The following: 168 | ```erlang 169 | f(A) -> A+1. 170 | ... 171 | test_tap() -> 172 | [10] / tap(f) 173 | / tap(fun f/1) 174 | / tap(fun(I) -> I+1 end). 175 | ``` 176 | is equivalent to: 177 | ```erlang 178 | ... 179 | test_tap() -> 180 | begin 181 | f(10), 182 | begin 183 | f(10), 184 | begin 185 | (fun(I) -> I+1 end)(10), 186 | 10 187 | end 188 | end 189 | end. 190 | ``` 191 | 192 | Some attempts to tackle this pipeline transform have been done by other developers: 193 | 194 | * https://github.com/fenollp/fancyflow 195 | * https://github.com/stolen/pipeline 196 | * https://github.com/oltarasenko/epipe 197 | * https://github.com/clanchun/epipe 198 | * https://github.com/pouriya/pipeline 199 | 200 | Yet, we subjectively believe that the choice of syntax in this implementation of transform 201 | is more succinct and elegant, and doesn't attempt to modify the meaning of the `/` operator 202 | for arithmetic LHS types (i.e. integers, floats, variables, and function calls). 203 | 204 | Why didn't we use `|>` operator instead of `/` to make it equivalent to Elixir? 205 | Parse transforms are applied only after the Erlang source code gets parsed to the AST 206 | representation, which must be in valid Erlang syntax. The `|>` operator is not known to 207 | the Erlang parser, and therefore, using it would result in the compile-time error. We 208 | had to select an operator that the Erlang parser would be happy with, and `/` was our choice 209 | because visually it resembles the pipe `|` character more than the other operators. 210 | 211 | ## `listcomp`: Fold and Indexed List Comprehensions 212 | 213 | ### Indexed List Comprehension 214 | 215 | Occasionally the body of a list comprehension needs to know the index 216 | of the current item in the fold. Consider this example: 217 | ```erlang 218 | [{1,10}, {2,20}] = element(1, lists:foldmapl(fun(I, N) -> {{N, I}, N+1} end, 1, [10,20])). 219 | ``` 220 | Here the `N` variable is tracking the index of the current item `I` in the list. 221 | While the same result in this specific case can be achieved with 222 | `lists:zip(lists:seq(1,2), [10,20])`, in a more general case, there is no way to have 223 | an item counter propagated with the current list comprehension syntax. 224 | 225 | The **Indexed List Comprehension** accomplishes just that through the use of an unassigned 226 | variable immediately to the right of the `||` operator: 227 | ```erlang 228 | [{Idx, I} || Idx, I <- L]. 229 | % ^^^ 230 | % | 231 | % +--- This variable becomes the index counter 232 | ``` 233 | Example: 234 | ```erlang 235 | [{1,10}, {2,20}] = [{Idx, I} || Idx, I <- [10,20]]. 236 | ``` 237 | 238 | ### Fold Comprehension 239 | 240 | To invoke the fold comprehension transform include the initial state 241 | assignment into a list comprehension: 242 | ```erlang 243 | [S+I || S = 1, I <- L]. 244 | % ^^^ ^^^^^ 245 | % | | 246 | % | +--- State variable bound to the initial value 247 | % +----------- The body of the foldl function 248 | ``` 249 | 250 | In this example the `S` variable gets assigned the initial state `1`, and 251 | the `S+I` expression represents the body of the fold function that 252 | is passed the iteration variable `I` and the state variable `S`: 253 | ```erlang 254 | lists:foldl(fun(I, S) -> S+I end, 1, L). 255 | ``` 256 | 257 | A fold comprehension can be combined with the indexed list comprehension 258 | by using this syntax: 259 | 260 | ```erlang 261 | [do(Idx, S+I) || Idx, S = 10, I <- L]. 262 | % ^^^^^^^^^^^^ ^^^ ^^^^^^ 263 | % | | | 264 | % | | +--- State variable bound to the initial value (e.g. 10) 265 | % | +--------- The index variable bound to the initial value of 1 266 | % +--------------------- The body of the foldl function can use Idx and S 267 | ``` 268 | 269 | This code is transformed to: 270 | ```erlang 271 | element(2, lists:foldl(fun(I, {Idx, S}) -> {Idx+1, do(Idx, S+I)} end, {1, 10}, L)). 272 | ``` 273 | 274 | Example: 275 | ```erlang 276 | 33 = [S + Idx*I || Idx, S = 1, I <- [10,20]], 277 | 278 | 30 = [print(Idx, I, S) || Idx, S=0, I <- [10,20]]. 279 | % Prints: 280 | % Item#1 running sum: 10 281 | % Item#2 running sum: 30 282 | 283 | print(Idx, I, S) -> 284 | Res = S+I, 285 | io:format("Item#~w running sum: ~w\n", [Idx, Res]), 286 | Res. 287 | ``` 288 | 289 | ## `iif`: Ternary and quaternary if 290 | 291 | This transform improves the code readability for cases that involve simple conditional 292 | `if/then/else` tests in the form `iif(Condition, Then, Else)`. Since this is a parse 293 | transform, the `Then` and `Else` expressions are evaluated **only** if the `Condition` 294 | evaluates to `true` or `false` respectively. 295 | 296 | E.g.: 297 | 298 | ```erlang 299 | iif(tuple_size(T) == 3, good, bad). %% Ternary if 300 | 301 | iif(some_fun(A), match, ok, error). %% Quaternary if 302 | 303 | nvl(L, undefined). 304 | 305 | nvl(L, nil, hd(L)) 306 | ``` 307 | 308 | are transformed to: 309 | 310 | ```erlang 311 | case tuple_size(T) == 3 of 312 | true -> good; 313 | _ -> bad 314 | end. 315 | 316 | case some_fun(A) of 317 | match -> ok; 318 | nomatch -> error 319 | end. 320 | 321 | case L of 322 | [] -> undefined; 323 | false -> undefined; 324 | undefined -> undefined; 325 | _ -> L 326 | end. 327 | 328 | case L of 329 | [] -> nil; 330 | false -> nil; 331 | undefined -> nil; 332 | _ -> hd(L) 333 | end. 334 | ``` 335 | 336 | ## `str`: String transforms 337 | 338 | This module implements a transform to stringify an Erlang term. 339 | 340 | * `str(Term)` is equivalent to `lists:flatten(io_lib:format("~p", [Term]))` for 341 | terms that are not integers, floats, atoms, binaries and lists. 342 | Integers, atoms, and binaries are converted to string using `*_to_list/1` 343 | functions. Floats are converted using `float_to_list/2` where the second 344 | argument is controled by `str:set_float_fmt/1` and `str:reset_float_fmt/0` 345 | calls. Lists are converted to string using 346 | `lists:flatten(io_lib:format("~s", [Term]))` and if that fails, then using 347 | `lists:flatten(io_lib:format("~p", [Term]))` format. 348 | * `str(Fmt, Args)` is equivalent to `lists:flatten(io_lib:format(Fmt, Args))`. 349 | * `bin(Fmt, Args)` is equivalent to `list_to_binary(lists:flatten(io_lib:format(Fmt, Args)))`. 350 | * `throw(Fmt,Args)` is equivalent to `throw(list_to_binary(io_lib:format(Fmt, Args)))`. 351 | * `error(Fmt,Args)` is equivalent to `error(list_to_binary(io_lib:format(Fmt, Args)))`. 352 | 353 | Two other shorthand transforms are optionally supported: 354 | 355 | * `b2l(Binary)` is equivalent to `binary_to_list(Binary)` (enabled by giving `{d,str_b2l}`) 356 | compilation option. 357 | * `i2l(Integer)` is equivalent to `integer_to_list(Binary)` (enabled by giving `{d,str_i2l}`) 358 | compilation option. 359 | 360 | E.g.: 361 | ``` 362 | erlc +debug_info -Dstr_b2l -Dstr_i2l +'{parse_transform, str}' -o ebin your_module.erl 363 | ``` 364 | 365 | ## Dowloading 366 | 367 | * [Github](https://github.com/saleyn/etran) 368 | * [Hex.pm](https://hex.pm/packages/etran) 369 | 370 | ## Building and Using 371 | 372 | ``` 373 | $ make 374 | ``` 375 | 376 | To use the transforms, compile your module with the `+'{parse_transform, Module}'` command-line 377 | option, or include `-compile({parse_transform, Module}).` in your source code, where `Module` 378 | is one of the transform modules implemented in this project. 379 | 380 | To use all transforms implemented by the `etran` application, compile your module with this 381 | command-line option: `+'{parse_transform, etran}'`. 382 | ``` 383 | erlc +debug_info +'{parse_transform, etran}' -o ebin your_module.erl 384 | ``` 385 | 386 | If you are using `rebar3` to build your project, then add to `rebar.config`: 387 | ``` 388 | {deps, [{etran, "0.5.1"}]}. 389 | 390 | {erl_opts, [debug_info, {parse_transform, etran}]}. 391 | ``` 392 | -------------------------------------------------------------------------------- /build-aux/google2792824a0f71459b.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google2792824a0f71459b.html -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, warn_export_all]}. 2 | {erl_first_files, ["etran_util.erl", "erlpipe.erl"]}. 3 | 4 | {project_plugins, [rebar3_hex, rebar3_ex_doc]}. 5 | 6 | {edoc_opts, [{title, "Collection of Erlang parse transforms"}, 7 | {overview, "build-aux/overview.edoc"}, 8 | {stylesheet_file,"build-aux/edoc.css"}, 9 | {keywords, "erlang, parse transform, pipe, pipeline, map-reduce, map, reduce, ternary, if, iif"}, 10 | {pretty_printer, erl_pp}, 11 | {app_default, "http://www.erlang.org/doc/man"}]}. 12 | 13 | {eunit_compile_opts, [{d,str_i2l},{d,str_b2l}]}. 14 | 15 | {hex, [{doc, ex_doc}]}. 16 | 17 | {ex_doc, [ 18 | {extras, [ 19 | {"README.md", #{title => "Overview"}}, 20 | {"LICENSE", #{title => "License"}} 21 | ]}, 22 | {main, "README.md"}, 23 | {source_url, "https://github.com/saleyn/etran"} 24 | ]}. 25 | -------------------------------------------------------------------------------- /src/defarg.erl: -------------------------------------------------------------------------------- 1 | %%% vim:ts=2:sw=2:et 2 | %%%----------------------------------------------------------------------------- 3 | %%% @doc Erlang parse transform for permitting default arguments in functions 4 | %%% 5 | %%% Presently the Erlang syntax doesn't allow function arguments to have default 6 | %%% parameters. Consequently a developer needs to replicate the function 7 | %%% definition multiple times passing constant defaults to some parameters of 8 | %%% functions. 9 | %%% 10 | %%% This parse transform addresses this shortcoming by extending the syntax 11 | %%% of function definitions at the top level in a module to have a default 12 | %%% expression such that for `A / Default' argument the `Default' will be 13 | %%% used if the function is called in code without that argument. 14 | %%% 15 | %%% ``` 16 | %%% -export([t/2]). 17 | %%% 18 | %%% test(A / 10, B / 20) -> 19 | %%% A + B. 20 | %%% ''' 21 | %%% The code above is transformed to: 22 | %%% ``` 23 | %%% -export([t/2]). 24 | %%% -export([t/0, t/1]). 25 | %%% 26 | %%% test() -> test(10); 27 | %%% test(A) -> test(A, 20); 28 | %%% test(A,B) -> A+B. 29 | %%% ''' 30 | %%% 31 | %%% The arguments with default values must be at the end of the argument list: 32 | %%% ``` 33 | %%% test(A, B, C / 1) -> %% This is valid 34 | %%% ... 35 | %%% 36 | %%% test(A / 1, B, C) -> %% This is invalid 37 | %%% ... 38 | %%% ''' 39 | %%% 40 | %%% Default arguments must be constants or arithmetic expressions. Function 41 | %%% calls are not supported as default arguments due to the limitations of the 42 | %%% Erlang parser. 43 | %%% 44 | %%% @author Serge Aleynikov 45 | %%% @end 46 | %%%----------------------------------------------------------------------------- 47 | %%% Copyright (c) 2021 Serge Aleynikov 48 | %%% 49 | %%% Permission is hereby granted, free of charge, to any person 50 | %%% obtaining a copy of this software and associated documentation 51 | %%% files (the "Software"), to deal in the Software without restriction, 52 | %%% including without limitation the rights to use, copy, modify, merge, 53 | %%% publish, distribute, sublicense, and/or sell copies of the Software, 54 | %%% and to permit persons to whom the Software is furnished to do 55 | %%% so, subject to the following conditions: 56 | %%% 57 | %%% The above copyright notice and this permission notice shall be included 58 | %%% in all copies or substantial portions of the Software. 59 | %%% 60 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 61 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 62 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 63 | %%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 64 | %%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 65 | %%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 66 | %%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 67 | %%%----------------------------------------------------------------------------- 68 | -module(defarg). 69 | 70 | -export([parse_transform/2]). 71 | 72 | %% @doc parse_transform entry point 73 | parse_transform(AST, Options) -> 74 | etran_util:process(?MODULE, 75 | fun(Ast) -> replace(Ast) end, 76 | AST, Options). 77 | 78 | replace(AST) -> 79 | ModExports = lists:sort(lists:append([Exp || {attribute, _, export, Exp} <- AST])), 80 | replace(AST, undefined, [], ModExports, []). 81 | 82 | replace([], _Mod, Exports, _ModExports, Acc) -> 83 | Res = lists:reverse(Acc), 84 | {HeadAST, [{attribute, Loc, _, _} = ModAST|TailAST]} = 85 | lists:splitwith(fun({attribute, _, module, _}) -> false; (_) -> true end, Res), 86 | AddExports = [{attribute, Loc, export, Exp} || Exp <- lists:reverse(Exports)], 87 | HeadAST ++ [ModAST] ++ AddExports ++ TailAST; 88 | 89 | replace([{attribute,_,module,Mod}=H|T], _, Exports, ModExports, Acc) -> 90 | replace(T, Mod, Exports, ModExports, [H|Acc]); 91 | 92 | replace([{function, Loc, Fun, Arity, [{clause, CLoc, Args, Guards, Body}]}=H|T], 93 | Mod, Exports, ModExports, Acc) -> 94 | {RevDef, RevRestArgs} = 95 | lists:splitwith( 96 | fun({op, _, '/', _Arg, _Def}) -> true; 97 | (_) -> false 98 | end, 99 | lists:reverse(Args)), 100 | {FrontArgs, DefArgs} = 101 | {lists:reverse(RevRestArgs), lists:reverse([{A,D} || {op, _, '/', A, D} <- RevDef])}, 102 | 103 | case DefArgs of 104 | [] -> 105 | replace(T, Mod, Exports, ModExports, [H|Acc]); 106 | _ -> 107 | lists:filter(fun({op, _, '/', _A, _D}) -> true; (_) -> false end, FrontArgs) /= [] 108 | andalso throw(lists:flatten( 109 | lists:format( 110 | "Function ~w:~w/~w has default arguments not at the end of the argument list!", 111 | [get(key), Fun, Arity]))), 112 | %% Add new exports, e.g.: -export([f/2]). 113 | N = Arity - length(DefArgs), 114 | NewExports = case lists:member({Fun,Arity}, ModExports) of 115 | true -> [[{Fun,I} || I <- lists:seq(N, Arity-1)] | Exports]; 116 | false -> Exports 117 | end, 118 | 119 | LastClause = {function, Loc, Fun, Arity, 120 | [{clause, CLoc, FrontArgs ++ [A || {A,_} <- DefArgs], Guards, Body}]}, 121 | 122 | AddClauses = element(3, 123 | lists:foldl(fun({A, D}, {Front, ArityN, Acc1}) -> 124 | Acc2 = [{function, Loc, Fun, ArityN, 125 | [{clause, CLoc, Front, [], 126 | [{call, CLoc, {atom, CLoc, Fun}, Front ++ [D]}]}]} | Acc1], 127 | {Front ++ [A], ArityN+1, Acc2} 128 | end, {FrontArgs, N, []}, DefArgs)), 129 | 130 | replace(T, Mod, NewExports, ModExports, [LastClause | AddClauses] ++ Acc) 131 | end; 132 | 133 | replace([H|T], Mod, Exports, ModExports, Acc) -> 134 | replace(T, Mod, Exports, ModExports, [H|Acc]). 135 | -------------------------------------------------------------------------------- /src/erlpipe.erl: -------------------------------------------------------------------------------- 1 | %%% vim:ts=2:sw=2:et 2 | %%%----------------------------------------------------------------------------- 3 | %%% @doc Erlang pipeline parse transform 4 | %%% 5 | %%% This transform implements a parser syntax extension that enables application 6 | %%% of cascading function calls using the `/' operator. 7 | %%% 8 | %%% In the `LHS / RHS / ... Last.' notation, the result of evaluation of the LHS 9 | %%% expression is passed as an argument to the RHS expression. This process 10 | %%% continues until the `Last' expression is evaluated. The head element of the 11 | %%% pipeline must be either a term to which the arithmetic division `/` operator 12 | %%% cannot apply (i.e. not integers, floats, functions), or if you need to pass 13 | %%% integer(s) or float(s), wrap them in a list brackets. 14 | %%% 15 | %%% This transfor is inspired by the similar functionality in Linux (i.e. `|' 16 | %%% pipe) and Elixir (`|>' pipe). 17 | %%% 18 | %%% When using this as a parse transform, include the `{parse_transform,erlpipe}' 19 | %%% compiler option. 20 | %%% 21 | %%% The following examples illustrate the work of the transform, in which: 22 | %%% ``` 23 | %%% test1(A) -> [A] / fun1 / mod:fun2 / fun3. 24 | %%% test2(A,B) -> [A,B] / fun4 / fun5() / io:format("~p\n", [_]). 25 | %%% ''' 26 | %%% will be transformed to: 27 | %%% ``` 28 | %%% test1(A) -> fun3(mod:fun2(fun1(A))). 29 | %%% test2(A,B) -> io:format("~p\n", [fun5(fun4(A,B))]). 30 | %%% ''' 31 | %%% 32 | %%% Similarly to Elixir, a special `tap/2' function is implemented, which 33 | %%% passes the given argument to an anonymous function, returning the argument 34 | %%% itself. The following: 35 | %%% ``` 36 | %%% f(A) -> A+1. 37 | %%% ... 38 | %%% test_tap() -> 39 | %%% [10] / tap(f) 40 | %%% / tap(fun f/1) 41 | %%% / tap(fun(I) -> I+1 end). 42 | %%% ''' 43 | %%% is equivalent to: 44 | %%% ``` 45 | %%% ... 46 | %%% test_tap() -> 47 | %%% begin 48 | %%% f(10), 49 | %%% begin 50 | %%% f(10), 51 | %%% begin 52 | %%% (fun(I) -> I end)(10) 53 | %%% 10 54 | %%% end 55 | %%% end 56 | %%% end. 57 | %%% ''' 58 | %%% 59 | %%% For debugging the AST of the resulting transform, pass the following 60 | %%% options to the `erlc' compiler: 61 | %%%
62 | %%%
  • `-Derlpipe_orig' - print the original AST before the transform
  • 63 | %%%
  • `-Derlpipe_ast' - print the transformed AST
  • 64 | %%%
  • `-Derlpipe_src' - print the resulting source code after the transform
  • 65 | %%%
    66 | %%% 67 | %%% @author Serge Aleynikov 68 | %%% @end 69 | %%%----------------------------------------------------------------------------- 70 | %%% Copyright (c) 2021 Serge Aleynikov 71 | %%% 72 | %%% Permission is hereby granted, free of charge, to any person 73 | %%% obtaining a copy of this software and associated documentation 74 | %%% files (the "Software"), to deal in the Software without restriction, 75 | %%% including without limitation the rights to use, copy, modify, merge, 76 | %%% publish, distribute, sublicense, and/or sell copies of the Software, 77 | %%% and to permit persons to whom the Software is furnished to do 78 | %%% so, subject to the following conditions: 79 | %%% 80 | %%% The above copyright notice and this permission notice shall be included 81 | %%% in all copies or substantial portions of the Software. 82 | %%% 83 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 84 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 85 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 86 | %%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 87 | %%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 88 | %%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 89 | %%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 90 | %%%----------------------------------------------------------------------------- 91 | -module(erlpipe). 92 | 93 | -export([parse_transform/2]). 94 | -import(etran_util, [transform/2]). 95 | 96 | -define(OP, '/'). 97 | 98 | %% @doc parse_transform entry point 99 | parse_transform(AST, Options) -> 100 | etran_util:apply_transform(?MODULE, fun replace/1, AST, Options). 101 | 102 | replace({op, _Loc, ?OP, Arg, Rhs}) -> 103 | apply_args(Arg, Rhs); 104 | replace(_Exp) -> 105 | continue. 106 | 107 | apply_args({op, _Loc, ?OP, A, E}, Rhs) -> 108 | case apply_args(A, E) of 109 | continue -> continue; 110 | [Args] -> apply_args(Rhs, [Args]); 111 | Args -> apply_args(Rhs, [Args]) 112 | end; 113 | apply_args({cons, _Loc, _, _} = List, Rhs) -> 114 | Args = [hd(transform(fun replace/1, [F])) || F <- cons_to_list(List)], 115 | [E] = transform(fun replace/1, [Rhs]), 116 | do_apply(E, Args); 117 | apply_args({Op, _Loc, _} = Arg, RHS) when Op==atom; Op==bin; Op==tuple; Op==string -> 118 | if is_tuple(RHS) -> 119 | do_apply(RHS, [Arg]); 120 | true -> 121 | do_apply(Arg, RHS) 122 | end; 123 | %% List comprehension 124 | apply_args({lc,_,_,_}=Lhs, Rhs) -> 125 | do_apply(Lhs, Rhs); 126 | apply_args(AArgs, Rhs) when is_list(AArgs), is_list(Rhs) -> 127 | Args = [hd(transform(fun replace/1, [F])) || F <- AArgs], 128 | [E] = transform(fun replace/1, Rhs), 129 | do_apply(E, Args); 130 | apply_args(LHS, RHS) when is_tuple(LHS), is_list(RHS) -> 131 | do_apply(LHS, RHS); 132 | apply_args(LHS, RHS) when is_tuple(LHS), is_tuple(RHS) -> 133 | continue. 134 | 135 | do_apply({atom, Loc, _V} = Function, Arguments) -> 136 | {call, Loc, Function, Arguments}; 137 | 138 | do_apply({remote, Loc, _M, _F} = Function, Arguments) -> 139 | {call, Loc, Function, Arguments}; 140 | 141 | do_apply({call, Loc, Fun, []}, Arguments) -> 142 | {call, Loc, Fun, Arguments}; 143 | 144 | do_apply({'fun', Loc, {function, Fun, _}}, Arguments) -> 145 | {call, Loc, {atom, Loc, Fun}, Arguments}; 146 | do_apply({'fun', Loc, {function, _Mod, _Fun, _Arity}=F}, Arguments) -> 147 | {call, Loc, {'fun', Loc, F}, Arguments}; 148 | do_apply({'fun', Loc, {clauses, _}}=Fun, Arguments) -> 149 | {call, Loc, Fun, Arguments}; 150 | 151 | do_apply({call, _Loc, {atom, ALoc, tap}, [Arg]}, RHS) -> 152 | %% Tapping into a function's call (the return is a passed-through RHS argument) 153 | Res = do_apply(Arg, RHS), 154 | % If we are asked to tap into the fun's call, wrap the call in a block 155 | {block, ALoc, [{match, ALoc, {var, ALoc, '_'}, Res}, hd(RHS)]}; 156 | 157 | %% RHS is a tuple when it's the head of a pipeline: 158 | %% E.g. [I || I <- L] / ... 159 | do_apply({Op, Loc, Fun, Args} = LHS, RHS) when (Op =:= call orelse Op =:= lc), is_list(RHS) -> 160 | [NewLHS] = transform(fun(Forms) -> substitute(RHS, Forms) end, [LHS]), 161 | case NewLHS of 162 | LHS -> 163 | {Op, Loc, Fun, RHS ++ Args}; 164 | ResLHS -> 165 | ResLHS 166 | end; 167 | 168 | %% RHS is a list when it's in the middle of a pipeline: 169 | %% E.g. ... / [I || I <- _] / ... 170 | do_apply({Op, _, _, _} = LHS, RHS) when (Op =:= call orelse Op =:= lc), is_tuple(RHS) -> 171 | do_apply(RHS, [LHS]); 172 | 173 | do_apply({Op, Loc, Fun, Args} = LHS, RHS) when Op =:= call; Op =:= lc -> 174 | [NewLHS] = transform(fun(Forms) -> substitute(RHS, Forms) end, [LHS]), 175 | case NewLHS of 176 | LHS when is_list(RHS) -> 177 | {Op, Loc, Fun, RHS ++ Args}; 178 | LHS -> 179 | do_apply(RHS, [LHS]); 180 | ResLHS -> 181 | ResLHS 182 | end; 183 | 184 | %% Use of operators 185 | do_apply({op, Loc, Op, Lhs, Rhs}, Arguments) -> 186 | NewLhs = transform(fun replace/1, [Lhs]), 187 | NewRhs = transform(fun replace/1, [Rhs]), 188 | [LHS] = transform(fun(Forms) -> substitute(Arguments, Forms) end, NewLhs), 189 | [RHS] = transform(fun(Forms) -> substitute(Arguments, Forms) end, NewRhs), 190 | {op, Loc, Op, LHS, RHS}; 191 | 192 | do_apply({var, _, '_'}, [Arg]) -> 193 | Arg; 194 | do_apply(Exp, _A) when is_tuple(Exp) -> 195 | Exp. 196 | 197 | cons_to_list({cons, _, A, B}) -> 198 | [A | cons_to_list(B)]; 199 | cons_to_list({nil, _}) -> 200 | []; 201 | cons_to_list([A]) -> 202 | [A]. 203 | 204 | %% Substitute '_', '_1', '_2', ... '_N' with the corresponding argument 205 | substitute(Args, {var, _, V}) when is_atom(V) -> 206 | case atom_to_list(V) of 207 | "_" when is_list(Args) -> 208 | hd(Args); 209 | "_" -> Args; 210 | [$_|T] -> 211 | try 212 | M = list_to_integer(T), 213 | lists:nth(M, Args) 214 | catch _:_ -> 215 | continue 216 | end; 217 | _ -> 218 | continue 219 | end; 220 | substitute(_Args, _) -> 221 | continue. 222 | -------------------------------------------------------------------------------- /src/etran.app.src: -------------------------------------------------------------------------------- 1 | {application,etran, 2 | [{description,"Erlang Parse Transoforms"}, 3 | {vsn,"0.5.2"}, 4 | {applications,[kernel,stdlib]}, 5 | {licenses,["MIT"]}, 6 | {links,[{"Github","https://github.com/saleyn/etran"}]}]}. 7 | -------------------------------------------------------------------------------- /src/etran.erl: -------------------------------------------------------------------------------- 1 | %%%----------------------------------------------------------------------------- 2 | %%% @doc Apply all transforms in the `etran' application 3 | %%% 4 | %%% @author Serge Aleynikov 5 | %%% @end 6 | %%%----------------------------------------------------------------------------- 7 | %%% Copyright (c) 2021 Serge Aleynikov 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person 10 | %%% obtaining a copy of this software and associated documentation 11 | %%% files (the "Software"), to deal in the Software without restriction, 12 | %%% including without limitation the rights to use, copy, modify, merge, 13 | %%% publish, distribute, sublicense, and/or sell copies of the Software, 14 | %%% and to permit persons to whom the Software is furnished to do 15 | %%% so, subject to the following conditions: 16 | %%% 17 | %%% The above copyright notice and this permission notice shall be included 18 | %%% in all copies or substantial portions of the Software. 19 | %%% 20 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 23 | %%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | %%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 25 | %%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 26 | %%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | %%%----------------------------------------------------------------------------- 28 | -module(etran). 29 | 30 | -export([parse_transform/2]). 31 | 32 | %% @doc parse_transform entry point 33 | parse_transform(AST, Options) -> 34 | A0 = defarg:parse_transform (AST, Options), 35 | A1 = erlpipe:parse_transform (A0, Options), 36 | A2 = iif:parse_transform (A1, Options), 37 | A3 = listcomp:parse_transform(A2, Options), 38 | A4 = str:parse_transform (A3, Options), 39 | A5 = gin_transform (A4, Options), 40 | A5. 41 | 42 | gin_transform(AST, Options) -> 43 | %% Apply the `gin' transform if it's found 44 | %% See: https://github.com/mad-cocktail/gin 45 | case code:which(gin) of 46 | non_existing -> 47 | AST; 48 | _ -> 49 | gin:parse_transform(AST, Options) 50 | end. 51 | -------------------------------------------------------------------------------- /src/etran_util.erl: -------------------------------------------------------------------------------- 1 | %%%----------------------------------------------------------------------------- 2 | %%% @doc Erlang parse transform utility functions 3 | %%% @author Serge Aleynikov 4 | %%% @end 5 | %%%----------------------------------------------------------------------------- 6 | %%% Copyright (c) 2021 Serge Aleynikov 7 | %%% 8 | %%% Permission is hereby granted, free of charge, to any person 9 | %%% obtaining a copy of this software and associated documentation 10 | %%% files (the "Software"), to deal in the Software without restriction, 11 | %%% including without limitation the rights to use, copy, modify, merge, 12 | %%% publish, distribute, sublicense, and/or sell copies of the Software, 13 | %%% and to permit persons to whom the Software is furnished to do 14 | %%% so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included 17 | %%% in all copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22 | %%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | %%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 24 | %%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 25 | %%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | %%%----------------------------------------------------------------------------- 27 | -module(etran_util). 28 | -export([transform/2, transform/3, apply_transform/4, process/4]). 29 | -export([parse_options/2, debug_options/2, source_forms/2]). 30 | 31 | %%------------------------------------------------------------------------------ 32 | %% @doc Transform `Forms' by applying a lambda `Fun'. 33 | %% @end 34 | %%------------------------------------------------------------------------------ 35 | -spec transform(fun((Forms::term()) -> tuple()|continue), Forms::term()) -> list(). 36 | transform(Fun, Forms) when is_function(Fun, 1), is_list(Forms) -> 37 | transform2(Fun, Forms); 38 | transform(Fun, Form) when is_function(Fun, 1), is_tuple(Form) -> 39 | transform2(Fun, [Form]). 40 | 41 | transform2(_, []) -> 42 | []; 43 | transform2(Fun, [L|Fs]) when is_list(L) -> 44 | [transform2(Fun, L) | transform2(Fun, Fs)]; 45 | transform2(Fun, [F|Fs]) when is_tuple(F), is_atom(element(1,F)) -> 46 | case Fun(F) of 47 | NewF when is_tuple(NewF) -> 48 | [NewF | transform2(Fun, Fs)]; 49 | continue -> 50 | [list_to_tuple(transform2(Fun, tuple_to_list(F))) | transform2(Fun, Fs)] 51 | end; 52 | transform2(Fun, [F|Fs]) -> 53 | [F | transform2(Fun, Fs)]; 54 | transform2(_, F) -> 55 | F. 56 | 57 | %%------------------------------------------------------------------------------ 58 | %% @doc Transform `Forms' by applying a lambda `Fun'. 59 | %% @end 60 | %%------------------------------------------------------------------------------ 61 | -spec transform(fun((Forms::term(), State::term()) -> 62 | {tuple()|continue, NewState::term()}), 63 | Forms::term(), State::term()) -> 64 | {list(), NewState::term()}. 65 | transform(Fun, Forms, State) when is_function(Fun, 2), is_list(Forms) -> 66 | transform2(Fun, Forms, State). 67 | 68 | transform2(_, [], State) -> 69 | {[], State}; 70 | transform2(Fun, [L|Fs], State) when is_list(L) -> 71 | {Res1, ResSt1} = transform2(Fun, L, State), 72 | {Res2, ResSt2} = transform2(Fun, Fs, ResSt1), 73 | {[Res1 | Res2], ResSt2}; 74 | transform2(Fun, [F|Fs], State) when is_tuple(F), is_atom(element(1,F)) -> 75 | case Fun(F, State) of 76 | {NewF, NewS} when is_tuple(NewF) -> 77 | {Res, ResSt} = transform2(Fun, Fs, NewS), 78 | {[NewF | Res], ResSt}; 79 | {continue, ResSt} -> 80 | {Res1, ResSt1} = transform2(Fun, tuple_to_list(F), ResSt), 81 | {Res2, ResSt2} = transform2(Fun, Fs, ResSt1), 82 | {[list_to_tuple(Res1) | Res2], ResSt2} 83 | end; 84 | transform2(Fun, [F|Fs], State) -> 85 | {Res, ResSt} = transform2(Fun, Fs, State), 86 | {[F | Res], ResSt}; 87 | transform2(_, F, State) -> 88 | {F, State}. 89 | 90 | %%------------------------------------------------------------------------------ 91 | %% @doc Apply parse transform with debug printing options 92 | %% @end 93 | %%------------------------------------------------------------------------------ 94 | apply_transform(Module, Fun, AST, Options) when is_atom(Module) 95 | , is_function(Fun, 1) 96 | , is_list(Options) -> 97 | process(Module, fun(Ast) -> transform(Fun, Ast) end, AST, Options). 98 | 99 | %%------------------------------------------------------------------------------ 100 | %% @doc Call `Fun' for the AST and optionally print debug info 101 | %% @end 102 | %%------------------------------------------------------------------------------ 103 | process(Module, Fun, AST, Options) when is_atom(Module) 104 | , is_function(Fun, 1) 105 | , is_list(Options) -> 106 | #{orig := OrigAST, ast := ResAST, src := SrcAST} = 107 | debug_options(Module, Options), 108 | 109 | OrigAST andalso io:format(">>> Before ~s:\n ~p~n", [Module, AST]), 110 | Transformed = Fun(AST), 111 | ResAST andalso io:format(">>> After ~s: ~p~n", [Module, Transformed]), 112 | SrcAST andalso source_forms(Transformed, 113 | [print, {format, ">>> Resulting Source:\n ~s~n"}]), 114 | Transformed. 115 | 116 | %%------------------------------------------------------------------------------ 117 | %% @doc Check if `KnownFlags' are found in Options. 118 | %% @end 119 | %%------------------------------------------------------------------------------ 120 | -spec parse_options(list(), list()) -> [boolean()]. 121 | parse_options(KnownFlags, Options) when is_list(KnownFlags), is_list(Options) -> 122 | [case lists:keyfind(I, 2, Options) of 123 | {d, I, Val} -> Val; 124 | {d, I} -> true; 125 | false -> false 126 | end || I <- KnownFlags]. 127 | 128 | %%------------------------------------------------------------------------------ 129 | %% @doc Get parse transforms debug options 130 | %% @end 131 | %%------------------------------------------------------------------------------ 132 | -spec debug_options(atom(), list()) -> 133 | #{orig => boolean(), ast => boolean(), src => boolean()}. 134 | debug_options(Module, Options) when is_atom(Module), is_list(Options) -> 135 | M = atom_to_list(Module), 136 | DbgOrig = list_to_atom(M ++ "_orig"), 137 | DbgAST = list_to_atom(M ++ "_ast"), 138 | DbgSrc = list_to_atom(M ++ "_src"), 139 | [OrigAST, ResAST, SrcAST] = 140 | parse_options([DbgOrig, DbgAST, DbgSrc], Options), 141 | #{orig => OrigAST, ast => ResAST, src => SrcAST}. 142 | 143 | %%------------------------------------------------------------------------------ 144 | %% @doc Decompile source code from the AST 145 | %% @end 146 | %%------------------------------------------------------------------------------ 147 | -spec source_forms(list(), list()) -> ok | string(). 148 | source_forms(AST, Options) -> 149 | Res = erl_prettypr:format(erl_syntax:form_list(tl(AST))), 150 | case lists:member(print, Options) of 151 | true -> 152 | io:format(proplists:get_value(format, Options, "~s\n"), [Res]); 153 | false -> 154 | Res 155 | end. 156 | -------------------------------------------------------------------------------- /src/iif.erl: -------------------------------------------------------------------------------- 1 | %%% vim:ts=2:sw=2:et 2 | %%%----------------------------------------------------------------------------- 3 | %%% @doc Conditional expression functions 4 | %%% 5 | %%% This module exports a parse transform and implements several 6 | %%% condition-checking functions. 7 | %%% 8 | %%% When using this as a parse transform, include the `{parse_transform,iif}' 9 | %%% compiler option. In this case for given expressions `A',`B',`C', and `D' 10 | %%% the following code transforms will be done: 11 | %%% ``` 12 | %%% iif(A, B) -> case A of true -> B; _ -> undefined end 13 | %%% iif(A, B, C) -> case A of true -> B; _ -> C end 14 | %%% iif(A,B,C,D) -> case A of B -> C; _ -> D end 15 | %%% nvl(A,B) -> case A of false -> B; undefined -> B; [] -> B; _ -> A end 16 | %%% nvl(A,B,C) -> case A of false -> B; undefined -> B; [] -> B; _ -> C end 17 | %%% ''' 18 | %%% For debugging the AST of the resulting transform, use `iif_debug' 19 | %%% command-line option: 20 | %%% ``` 21 | %%% erlc -Diif_debug=1 ... % Prints AST before the transform 22 | %%% erlc -Diif_debug=2 ... % Prints AST after the transform 23 | %%% erlc -Diif_debug[=3] ... % Prints AST before/after the transform 24 | %%% ''' 25 | %%% 26 | %%% Alternative to using this module as a parse_transform, it implements 27 | %%% several `iif/3,4' exported functions that can be used without the transform. 28 | %%% 29 | %%% @author Serge Aleynikov 30 | %%% @end 31 | %%%----------------------------------------------------------------------------- 32 | %%% Copyright (c) 2015 Serge Aleynikov 33 | %%% 34 | %%% Permission is hereby granted, free of charge, to any person 35 | %%% obtaining a copy of this software and associated documentation 36 | %%% files (the "Software"), to deal in the Software without restriction, 37 | %%% including without limitation the rights to use, copy, modify, merge, 38 | %%% publish, distribute, sublicense, and/or sell copies of the Software, 39 | %%% and to permit persons to whom the Software is furnished to do 40 | %%% so, subject to the following conditions: 41 | %%% 42 | %%% The above copyright notice and this permission notice shall be included 43 | %%% in all copies or substantial portions of the Software. 44 | %%% 45 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 46 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 47 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 48 | %%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 49 | %%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 50 | %%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 51 | %%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 52 | %%%----------------------------------------------------------------------------- 53 | -module(iif). 54 | 55 | % If using this module as a parse transform, we need to export the following: 56 | -export([parse_transform/2]). 57 | 58 | -export([iif/3, iif/4, nvl/2, ife/2, ife/3, ifne/2, ifne/3, format_ne/3]). 59 | 60 | -ifdef(TEST). 61 | -include_lib("eunit/include/eunit.hrl"). 62 | -endif. 63 | 64 | %%%----------------------------------------------------------------------------- 65 | %%% External API 66 | %%%----------------------------------------------------------------------------- 67 | 68 | %% @doc Parse transform to be used by providing `{parse_transform, iif}' option. 69 | %% `Opts' are compiler options passed from command line. E.g.: 70 | %% ``` 71 | %% erlc -Diif_ast ... -> Opts = [{d,iif_ast}|_] 72 | %% erlc -Diif_orig ... -> Opts = [{d,iif_orig}|_] 73 | %% erlc -Diif_src ... -> Opts = [{d,iif_src}|_] 74 | %% ''' 75 | parse_transform(AST, Opts) -> 76 | etran_util:process(?MODULE, fun(Ast) -> replace(Ast) end, AST, Opts). 77 | 78 | replace(AST) -> 79 | Tree = erl_syntax:form_list(AST), 80 | put(count, 1), 81 | try 82 | ModifiedTree = recurse(Tree, #{}), 83 | erase(line), 84 | erase(count), 85 | erl_syntax:revert_forms(ModifiedTree) 86 | catch E:R:S -> 87 | io:format(standard_error, "Error transforming AST: ~p\n ~p\n", [R, S]), 88 | erlang:raise(E,R,S) 89 | end. 90 | 91 | %% @doc Return `True' if first argument is `true' or return `False' if 92 | %% the first argument is one of: `[]', `false', `undefined'. 93 | iif([], _True, False) -> execute([], False); 94 | iif(false, _True, False) -> execute([], False); 95 | iif(undefined, _True, False) -> execute([], False); 96 | iif(true, True,_False) -> execute([], True). 97 | 98 | %% @doc Return `True' if first two arguments match 99 | iif(Value, Value, True,_False) -> execute(Value, True); 100 | iif(Value,_Other, _True, False) -> execute(Value, False). 101 | 102 | %% @doc Alias for `ife/2' 103 | nvl(Value, IfNull) -> ife(Value, IfNull). 104 | 105 | %% @doc Return `Value' if first argument is one of: `[]', `false', `undefined'. 106 | %% Otherwise return the value of the first argument. 107 | ife([], Value) -> execute([], Value); 108 | ife(false, Value) -> execute([], Value); 109 | ife(undefined, Value) -> execute([], Value); 110 | ife(Test, _Value) -> Test. 111 | 112 | %% @doc Return `Empty' if first argument is one of: `[]', `false', `undefined'. 113 | %% Otherwise, if `NotEmpty' is `fun()', evaluate it, or if it's `fun(Arg)' 114 | %% evaluate it with `Value' argument. 115 | ife([], Empty,_NotEmpty) -> execute([], Empty); 116 | ife(false, Empty,_NotEmpty) -> execute([], Empty); 117 | ife(undefined, Empty,_NotEmpty) -> execute([], Empty); 118 | ife(Value, _Empty, NotEmpty) -> execute(Value, NotEmpty). 119 | 120 | %% @doc Return `Value' if first argument is not one of: `[]', `false', `undefined'. 121 | %% Otherwise, if `Value' is `fun()', evaluate it, or if it's `fun(Arg)' 122 | %% evaluate it with `Test' argument. 123 | % If not empty 124 | ifne([], _Value) -> []; 125 | ifne(false, _Value) -> []; 126 | ifne(undefined,_Value) -> []; 127 | ifne(Test, Value) -> execute(Test, Value). 128 | 129 | %% @doc Return `NotEmpty' if first argument is not one of: `[]', `false', `undefined'. 130 | %% Otherwise, if `NotEmpty' is `fun()', evaluate it, or if it's `fun(Arg)' 131 | %% evaluate it with `Value' argument. 132 | ifne([], _NotEmpty, Empty) -> execute([], Empty); 133 | ifne(false, _NotEmpty, Empty) -> execute([], Empty); 134 | ifne(undefined,_NotEmpty, Empty) -> execute([], Empty); 135 | ifne(Value, NotEmpty,_Empty) -> execute(Value, NotEmpty). 136 | 137 | %% @doc Format if first argument is not empty 138 | format_ne(false, _Fmt, _Args) -> []; 139 | format_ne([], _Fmt, _Args) -> []; 140 | format_ne(_True, Fmt, Args) -> io_lib:format(Fmt, Args). 141 | 142 | %%%----------------------------------------------------------------------------- 143 | %%% Internal functions 144 | %%%----------------------------------------------------------------------------- 145 | 146 | execute(_, F) when is_function(F,0) -> F(); 147 | execute(V, F) when is_function(F,1) -> F(V); 148 | execute(_, V) -> V. 149 | 150 | %% Parse transform support 151 | recurse(Tree, Opts) -> 152 | update(case erl_syntax:subtrees(Tree) of 153 | [] -> Tree; 154 | List -> erl_syntax:update_tree(Tree, [[recurse(Subtree, Opts) || Subtree <- Group] 155 | || Group <- List]) 156 | end, 157 | Opts). 158 | 159 | %set_pos([H|T], Line) -> 160 | % [set_pos(H, Line) | set_pos(T, Line)]; 161 | %set_pos([], _Line) -> 162 | % []; 163 | %set_pos(T, Line) when is_tuple(T), tuple_size(T) > 1 -> 164 | % setelement(2, T, Line). 165 | 166 | clause3(A,B,C) -> clause3(A,B,C,get(line)). 167 | clause3(A,B,C, Line) -> erl_syntax:set_pos(erl_syntax:clause(A,B,C), Line). 168 | 169 | syn_atom(A) -> syn_atom(A, get(line)). 170 | syn_atom(A, Line) -> erl_syntax:set_pos(erl_syntax:atom(A), Line). 171 | syn_var (V) -> syn_var(V, get(line)). 172 | syn_var (V, Line) -> erl_syntax:set_pos(erl_syntax:variable(V), Line). 173 | %syn_if (V,Then,Else) -> L=get(line), erl_syntax:set_pos(erl_syntax:if_expr( 174 | % [clause3([],[[V]],[Then],L), 175 | % clause3([],[[syn_atom('true',L)]],[Else], L)]), 176 | % L). 177 | syn_case (A, Clauses) -> erl_syntax:set_pos(erl_syntax:case_expr(A, Clauses), get(line)). 178 | syn_case (V,M,T,F) -> syn_case(V, [clause3([M], [], [T]), 179 | clause3([syn_var('_')], [], [F])]). 180 | syn_block(Clauses) -> erl_syntax:set_pos(erl_syntax:block_expr(Clauses), get(line)). 181 | syn_match(A, B) -> erl_syntax:set_pos(erl_syntax:match_expr(A, B), get(line)). 182 | syn_nil() -> erl_syntax:set_pos(erl_syntax:nil(), get(line)). 183 | 184 | make_var_name({I,_} = Line) -> 185 | K = get(count), 186 | put(count, K+1), 187 | syn_var(list_to_atom(lists:append(["__I@",integer_to_list(I),"_",integer_to_list(K)])), Line). 188 | 189 | update(Node, _Opts) -> 190 | case erl_syntax:type(Node) of 191 | application -> 192 | case erl_syntax:application_operator(Node) of 193 | {atom, Line, iif} -> 194 | put(line, Line), 195 | %io:format("Application: Op=~p ~1024p\n", [erl_syntax:application_operator(Node), erl_syntax:application_arguments(Node)]), 196 | case erl_syntax:application_arguments(Node) of 197 | [A,B] -> 198 | %% This is a call to iif(A, B). 199 | %% Replace it with: 200 | %% case A of true -> B; _ -> undefined end 201 | syn_case(A, {atom, Line, true}, B, {atom, Line, undefined}); 202 | [A,B,C] -> 203 | %% This is a call to iif(A, B, C). 204 | %% Replace it with: 205 | %% case A of true -> B; _ -> C end 206 | syn_case(A, {atom, Line, true}, B, C); 207 | [A,B,C,D] -> 208 | %% This is a call to iif(A, B, C, D). 209 | %% Replace it with: 210 | %% case A of B -> C; _ -> D end 211 | syn_case (A, B, C, D); 212 | _ -> 213 | Node 214 | end; 215 | {atom, Line, nvl} -> 216 | put(line, Line), 217 | case erl_syntax:application_arguments(Node) of 218 | [A,B] -> 219 | %% This is a call to ife(A, B). 220 | %% Replace it with a case expression: 221 | %% begin 222 | %% _V = A, 223 | %% case _V of false -> B; undefined -> B; [] -> B; _ -> A end 224 | %% end 225 | Var = make_var_name(Line), 226 | syn_block([ 227 | syn_match(Var, A), 228 | syn_case(Var, 229 | [clause3([syn_atom(false) ],[],[B]), 230 | clause3([syn_atom(undefined)],[],[B]), 231 | clause3([syn_nil()], [],[B]), 232 | clause3([syn_var('_')], [],[A])]) 233 | ]); 234 | [A,B,C] -> 235 | %% This is a call to nvl(A, B, C). 236 | %% Replace it with a case expression: 237 | %% begin 238 | %% _V = A, 239 | %% case _V of false -> B; undefined -> B; [] -> B; _ -> C end 240 | %% end 241 | Var = make_var_name(Line), 242 | syn_block([ 243 | syn_match(Var, A), 244 | syn_case(Var, 245 | [clause3([syn_atom(false) ],[],[B]), 246 | clause3([syn_atom(undefined)],[],[B]), 247 | clause3([syn_nil()], [],[B]), 248 | clause3([syn_var('_')], [],[C])]) 249 | ]); 250 | _ -> 251 | Node 252 | end; 253 | _ -> 254 | Node 255 | end; 256 | _ -> 257 | Node 258 | end. 259 | 260 | %%%----------------------------------------------------------------------------- 261 | %%% Unit Tests 262 | %%%----------------------------------------------------------------------------- 263 | 264 | -ifdef(EUNIT). 265 | 266 | ife_test() -> 267 | ?assertEqual(abc, ife ([], abc)), 268 | ?assertEqual(abc, ife (false, abc)), 269 | ?assertEqual(abc, ife (undefined, abc)), 270 | ?assertEqual(xxx, ife (xxx, abc)), 271 | ?assertEqual(ok, ife (false, fun() -> ok end)), 272 | ?assertEqual([], ife (false, fun(V) -> V end)), 273 | 274 | ?assertEqual(abc, ife ([], abc, efg)), 275 | ?assertEqual(abc, ife (false, abc, efg)), 276 | ?assertEqual(abc, ife (undefined, abc, efg)), 277 | ?assertEqual(efg, ife (xxx, abc, efg)), 278 | ?assertEqual(xxx, ife (xxx, abc, fun(V) -> V end)). 279 | 280 | ifne_test() -> 281 | ?assertEqual([], ifne([], abc)), 282 | ?assertEqual([], ifne(false, abc)), 283 | ?assertEqual([], ifne(undefined, abc)), 284 | ?assertEqual(abc, ifne(xxx, abc)), 285 | ?assertEqual(ok, ifne(false, abc, fun() -> ok end)), 286 | ?assertEqual(x, ifne(xxx, fun() -> x end, efg)), 287 | ?assertEqual(xxx, ifne(xxx, fun(V) -> V end, efg)), 288 | 289 | ?assertEqual(efg, ifne([], abc, efg)), 290 | ?assertEqual(efg, ifne(false, abc, efg)), 291 | ?assertEqual(efg, ifne(undefined, abc, efg)), 292 | ?assertEqual(abc, ifne(xxx, abc, efg)), 293 | ?assertEqual(xxx, ifne(xxx, fun(V) -> V end, efg)). 294 | 295 | iif_test() -> 296 | ?assertEqual(abc, iif(x, x, abc, efg)), 297 | ?assertEqual(ok, iif(x, x, fun() -> ok end, efg)), 298 | ?assertEqual(x, iif(x, x, fun(X) -> X end, efg)), 299 | ?assertEqual(efg, iif(x, y, abc, efg)), 300 | ?assertEqual(ok, iif(x, y, abc, fun() -> ok end)), 301 | ?assertEqual(x, iif(x, y, abc, fun(X) -> X end)). 302 | 303 | -endif. 304 | -------------------------------------------------------------------------------- /src/listcomp.erl: -------------------------------------------------------------------------------- 1 | %%% vim:ts=2:sw=2:et 2 | %%%----------------------------------------------------------------------------- 3 | %%% @doc Erlang map-reduce parse transform 4 | %%% 5 | %%% This transform introduces two modifications of the list comprehension syntax 6 | %%% that allow to perform a fold and mapfold on a list. 7 | %%% 8 | %%% ==== Indexed List Comprehension ==== 9 | %%% 10 | %%% This extension of a list comprehension, passes an additional argument to the 11 | %%% left hand side of the comprehension, which is the index of the current item 12 | %%% in the list: 13 | %%% ``` 14 | %%% [ io:format("Rec#~w: ~p\n", [I, N]) || I, N <- L] 15 | %%% ^^ 16 | %%% ``` 17 | %%% The index is defined by the a variable listed after the `||' operator. 18 | %%% This is equivalent to the following: 19 | %%% ``` 20 | %%% lists:mapfoldl( 21 | %%% fun(N, I) -> 22 | %%% io:format("Rec#~w: ~p\n", [I, N]), 23 | %%% I+1 24 | %%% end, 1, L) 25 | %%% ``` 26 | %%% 27 | %%% === Fold Comprehension === 28 | %%% 29 | %%% To invoke the fold comprehension transform include the initial state 30 | %%% assignment into a comprehension that returns a non-tuple expression: 31 | %%% ``` 32 | %%% [S+N || S = 1, N <- L]. 33 | %%% ^^^ ^^^^^ 34 | %%% ''' 35 | %%% 36 | %%% In this example the `S' variable gets assigned the initial state `1', and 37 | %%% the `S+N' expression represents the body of the fold function that 38 | %%% is passed the iteration variable `N' and the state variable `S': 39 | %%% ``` 40 | %%% lists:foldl(fun(N, S) -> S+N end, 1, L). 41 | %%% ''' 42 | %%% 43 | %%% Fold comprehension can be combined with the indexed list comprehension: 44 | %%% ``` 45 | %%% [running_sum(I, N, S+N) || I, S=5, N <- L]. 46 | %%% 47 | %%% running_sum(I, N, RunningSum) -> 48 | %%% io:format("Rec#~w: ~p (~w)\n", [I, N, RunningSum]), 49 | %%% S. 50 | %%% ''' 51 | %%% 52 | %%% In this case the definition of the indexed fold comprehension would be 53 | %%% transformed to: 54 | %%% ``` 55 | %%% element(2, lists:foldl(fun(I, {N, S}) -> 56 | %%% running_sum(I, N, S), {N+1, S+N} end, {1,5}, L)), 57 | %%% ''' 58 | %%% 59 | %%% == Compilation == 60 | %%% 61 | %%% When using this as a parse transform, include the 62 | %%% `{parse_transform,listcomp}' compiler option. 63 | %%% 64 | %%% For debugging the AST of the resulting transform, pass the following 65 | %%% options to the `erlc' compiler: 66 | %%%
    67 | %%%
  • `-Dlistcomp_orig' - print the original AST before the transform
  • 68 | %%%
  • `-Dlistcomp_ast' - print the transformed AST
  • 69 | %%%
  • `-Dlistcomp_src' - print the resulting source code after the transform
  • 70 | %%%
    71 | %%% 72 | %%% @author Serge Aleynikov 73 | %%% @end 74 | %%%----------------------------------------------------------------------------- 75 | %%% Copyright (c) 2021 Serge Aleynikov 76 | %%% 77 | %%% Permission is hereby granted, free of charge, to any person 78 | %%% obtaining a copy of this software and associated documentation 79 | %%% files (the "Software"), to deal in the Software without restriction, 80 | %%% including without limitation the rights to use, copy, modify, merge, 81 | %%% publish, distribute, sublicense, and/or sell copies of the Software, 82 | %%% and to permit persons to whom the Software is furnished to do 83 | %%% so, subject to the following conditions: 84 | %%% 85 | %%% The above copyright notice and this permission notice shall be included 86 | %%% in all copies or substantial portions of the Software. 87 | %%% 88 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 89 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 90 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 91 | %%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 92 | %%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 93 | %%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 94 | %%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 95 | %%%----------------------------------------------------------------------------- 96 | -module(listcomp). 97 | 98 | -export([parse_transform/2]). 99 | -export([foldl/3, foldr/3]). 100 | 101 | -import(etran_util, [transform/2]). 102 | 103 | %% @doc parse_transform entry point 104 | parse_transform(AST, Options) -> 105 | etran_util:apply_transform(?MODULE, fun replace/1, AST, Options). 106 | 107 | %%------------------------------------------------------------------------------ 108 | %% @doc Fold over a list by additionally passing the list's current item number 109 | %% to the folding fun. This function is similar to lists:foldl/3, except 110 | %% that the fun takes the extra second integer argument that represents 111 | %% the sequential number of the item from the list. 112 | %% @end 113 | %%------------------------------------------------------------------------------ 114 | -spec foldl(fun((Position::integer(), Item::term(), Acc::term()) -> NewAcc::term()), 115 | Init::term(), list()) -> term(). 116 | foldl(Fun, Init, List) when is_function(Fun, 3) -> 117 | element(2, lists:foldl(fun(V, {I, S}) -> R = Fun(V, I, S), {I+1, R} end, {1, Init}, List)). 118 | 119 | %%------------------------------------------------------------------------------ 120 | %% @doc Fold over a list by additionally passing the list's current item number 121 | %% to the folding fun. This function is similar to lists:foldr/3, except 122 | %% that the fun takes the extra second integer argument that represents 123 | %% the sequential number of the item from the list. 124 | %% @end 125 | %%------------------------------------------------------------------------------ 126 | -spec foldr(fun((Position::integer(), Item::term(), Acc::term()) -> NewAcc::term()), 127 | Init::term(), list()) -> term(). 128 | foldr(Fun, Init, List) when is_function(Fun, 3) -> 129 | N = length(List), 130 | element(2, lists:foldr(fun(V, {I, S}) -> R = Fun(V, I, S), {I-1, R} end, {N, Init}, List)). 131 | 132 | %% Fold Transform 133 | %% ============== 134 | %% Example: 135 | %% L = [1,2,3] 136 | %% [S+I || S = 0, I <- L]. %% Returns: 6 137 | %% ^^^^^ 138 | %% [do(Index,S+I) || Index, S = 0, I <- L]. %% Returns: 6, Uses Index as the item index 139 | %% ^^^^^ ^^^^^ 140 | %% do(N, Sum) -> 141 | %% io:format("Item#~w running sum: ~w\n", [N, Sum]), 142 | %% Sum. 143 | %% 144 | %% [S+I || S = 0, I <- L] 145 | %% Rewrite: lists:foldl(fun(I, S) -> S+I end, 1, L). 146 | %% [S+I || N, S = 0, I <- L] 147 | %% Rewrite: lists:foldl(fun(I, S) -> S+I end, 1, L). 148 | %% [S+I || S = 1, I <- L, I > 10] 149 | %% Rewrite: lists:foldl(fun(I, S) -> S+I end, 1, [_I || _I <- L, _I > 10]). 150 | %% [S+I+J || S = 1, I <- L1, J <- L2] 151 | %% Rewrite: lists:foldl(fun({I,J}, S) -> S+I+J end, 1, [{I,J} || I <- L1, J <- L2]). 152 | %% 153 | replace({lc,Loc,ResBody0, 154 | [{var, _, V}=Var, 155 | {match,_,{var,_,_},_StateInit0}=Match | Generators]}) 156 | when is_atom(V) 157 | , element(1, ResBody0) /= tuple 158 | , element(1, hd(Generators)) == generate -> 159 | replace2(Loc, ResBody0, Var, Match, Generators); 160 | 161 | replace({lc,Loc,ResBody0, 162 | [{match,_,{var,_,_},_StateInit0}=Match | Generators]}) 163 | when element(1, ResBody0) /= tuple 164 | , element(1, hd(Generators)) == generate -> 165 | replace2(Loc, ResBody0, undefined, Match, Generators); 166 | 167 | %% Indexed list comprehension 168 | %% ========================== 169 | %% [I || _Index, I <- L1] 170 | %% Rewrite: element(1, lists:mapfoldl(fun(I, _Index) -> {I, _Index+1} end, 1, L)). 171 | replace({lc,Loc,ResBody0, [{var, _, V}=Var | Generators]}) 172 | when is_atom(V) 173 | , element(1, hd(Generators)) == generate -> 174 | replace2(Loc, ResBody0, Var, undefined, Generators); 175 | 176 | replace(_Exp) -> 177 | continue. 178 | 179 | maybe_make_var(_, {var,_,_} = Arg) -> 180 | {Arg, Arg}; 181 | maybe_make_var({Ln,Pos}=Loc, Arg) -> 182 | Var = {var,Loc,list_to_atom("_I@"++integer_to_list(Ln)++"_"++integer_to_list(Pos))}, 183 | Match = {match, Loc, Var, Arg}, 184 | {Var, Match}. 185 | 186 | replace2(Loc, ResBody0, Index, Match, Generators) -> 187 | [FunBody] = transform(fun replace/1, [ResBody0]), 188 | {Init,StateVar} = 189 | case {Index, Match} of 190 | {{var,_,_}, {match,_,{var,VLoc,_}=StateVar0,StateInit0}} -> 191 | %% [S+I || N, S = 5, I <- L, I > 10] 192 | %% Rewrite fold init, and state passed to fun: 193 | %% Init: {1, 5} 194 | %% State: {N, S} 195 | [Init0] = transform(fun replace/1, [StateInit0]), 196 | {{tuple, VLoc, [{integer, VLoc, 1}, Init0]}, 197 | {tuple, VLoc, [Index, StateVar0]}}; 198 | {undefined, {match,_,{var, _, _}=StateVar0,StateInit0}} -> 199 | %% [S+I || S = 0, I <- L] 200 | %% Rewrite: 201 | %% Init: 0 202 | %% State: S 203 | [Init0] = transform(fun replace/1, [StateInit0]), 204 | {Init0, StateVar0}; 205 | {{var,VLoc,_}, undefined} -> 206 | %% [I || N, I <- L] 207 | %% Rewrite: 208 | %% Init: 1 209 | %% State: N 210 | {{integer, VLoc, 1}, undefined} 211 | end, 212 | 213 | % Split generators from filters 214 | {Gens, Filters} = 215 | lists:splitwith(fun(G) -> element(1, G) == generate end, Generators), 216 | {GLoc, FunArgs, ListOfLCs} = 217 | case Gens of 218 | [{generate, Loc0, FunArg0, List0}] when Filters == [] -> 219 | % Simple case with one generator and no filters: 220 | % [S+I || S = 1, I <- L] 221 | {Loc0, FunArg0, List0}; 222 | [{generate, Loc0, FunArg0, List0}] when Filters /= [] -> 223 | % Simple case with one generator and no filters: 224 | % [S+I || S = 1, {I,_} <- L, I > 1, I < 10] 225 | % Convert the comprehension with filter into: 226 | % lists:mapfoldl(fun({I,_}, S) -> {I,S+I} end, 1, [_V || _V = {I,_} <- L, I > 1, I < 10]) 227 | {VarForm, MatchForm} = maybe_make_var(Loc0, FunArg0), 228 | {Loc0, FunArg0, {lc, Loc0, VarForm, 229 | [{generate, Loc0, MatchForm, List0}| Filters]}}; 230 | _ -> 231 | % More than one generator: 232 | % [S+I || S = 1, I = FunArg1 <- List1, J = FunArg2 <- List2, ...] 233 | % - Make a list: 234 | % [{I,FunArg1,LCList1}, {J,FunArg2,LCList2}, ...] 235 | VarsList = lists:reverse( 236 | lists:foldl(fun({generate, GLoc, FunArg0, List0}, ALists) -> 237 | {VarForm,MatchForm} = maybe_make_var(GLoc, FunArg0), 238 | [{VarForm, MatchForm, List0}|ALists] 239 | end, [], Gens)), 240 | 241 | % - Create a new list comprehension: 242 | % [{I,J,...} || I <- L1, J <- L2, ..., Filters] 243 | Vars = [V || {V,_,_} <- VarsList], 244 | ArgVars = {tuple, Loc, Vars}, 245 | FArgs = {tuple, Loc, [A || {_,A,_} <- VarsList]}, 246 | ListLCs = {lc, Loc, ArgVars, 247 | [{generate, GLoc, _Match, LList} 248 | || {{var,GLoc,_}, _Match, LList} <- VarsList] ++ Filters}, 249 | {Loc, FArgs, ListLCs} 250 | end, 251 | 252 | CallMapFoldl = fun(InitForm, StateForm, FoldFun, FunBodyForm) -> 253 | % lists:FoldFun(fun(FunArgs, StateForm) -> FunBodyForm end, 254 | % InitForm, _ListOfLCs = [{I,J,...} || I <- L1, J <- L2, ...]) 255 | {call, GLoc, 256 | {remote,GLoc,{atom,GLoc,lists},{atom,GLoc,FoldFun}}, 257 | [{'fun', GLoc, 258 | {clauses, 259 | [{clause, Loc, 260 | [FunArgs, StateForm], % The fun has 2 arguments: ({I,J, ...}, S) -> ... 261 | [], % No guards 262 | [FunBodyForm] % Body 263 | }]}}, 264 | InitForm, 265 | ListOfLCs]} 266 | end, 267 | 268 | % Finally, rewrite the call: 269 | case {Index,Match} of 270 | {{var,_,_}, {match,_,{var,_VLoc,_}, _}} -> 271 | % For [FunBody || N, StateVar = Init, ...] 272 | % produce: element(2, lists:foldl(fun(I, {N,StateVar}) -> {N+1,FunBody} end, {1,Init}, L)). 273 | FunBodyForm = 274 | {tuple, _VLoc, [{op, _VLoc, '+', Index, {integer, _VLoc, 1}}, FunBody]}, 275 | {call, GLoc, 276 | {atom, GLoc, element}, 277 | [{integer, GLoc, 2}, CallMapFoldl(Init, StateVar, foldl, FunBodyForm)]}; 278 | 279 | {undefined, {match,_,{var,_,_}, _}} -> 280 | % For [FunBody || StateVar = Init, I <- L, ...] 281 | % produce: lists:foldl(fun(I, S) -> FunBody end, 1, L). 282 | CallMapFoldl(Init, StateVar, foldl, FunBody); 283 | 284 | {{var, _VLoc, _}, undefined} -> 285 | % For [FunBody || N, I <- L] 286 | % produce: element(1, lists:mapfoldl(fun(FunBody, N) -> {FunBody, N+1} end, 1, L)). 287 | FunBodyForm = 288 | {tuple, _VLoc, [FunBody, {op, _VLoc, '+', Index, {integer, _VLoc, 1}}]}, 289 | {call, GLoc, 290 | {atom, GLoc, element}, 291 | [{integer, GLoc, 1}, 292 | CallMapFoldl(Init, Index, mapfoldl, FunBodyForm)]} 293 | end. 294 | -------------------------------------------------------------------------------- /src/str.erl: -------------------------------------------------------------------------------- 1 | %%% vim:ts=2:sw=2:et 2 | %%%----------------------------------------------------------------------------- 3 | %%% @doc Parse transform that implements `str/2' 4 | %%% 5 | %%% Use `{parse_transform,str}' compiler's option to use this transform. 6 | %%% ``` 7 | %%% str(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)) 8 | %%% bin(Fmt, Args) -> list_to_binary(io_lib:format(Fmt, Args)) 9 | %%% throw(Fmt, Args) -> erlang:throw(list_to_binary(io_lib:format(Fmt, Args)) 10 | %%% error(Fmt, Args) -> erlang:error(list_to_binary(io_lib:format(Fmt, Args)) 11 | %%% i2l(Int) -> integer_to_list(Int) % Enabled with compiled with 12 | %%% % the `{d,str_i2l}' option 13 | %%% b2l(Bin) -> binary_to_list(Bin) % Enabled with compiled with 14 | %%% % the `{d,str_b2l}' option 15 | %%% str(Term) -> str:str(Term) 16 | %%% bin(Term) -> str:bin(Term) 17 | %%% ''' 18 | %%% @author Serge Aleynikov 19 | %%% @end 20 | %%%----------------------------------------------------------------------------- 21 | %%% Copyright (c) 2021 Serge Aleynikov 22 | %%% 23 | %%% Permission is hereby granted, free of charge, to any person 24 | %%% obtaining a copy of this software and associated documentation 25 | %%% files (the "Software"), to deal in the Software without restriction, 26 | %%% including without limitation the rights to use, copy, modify, merge, 27 | %%% publish, distribute, sublicense, and/or sell copies of the Software, 28 | %%% and to permit persons to whom the Software is furnished to do 29 | %%% so, subject to the following conditions: 30 | %%% 31 | %%% The above copyright notice and this permission notice shall be included 32 | %%% in all copies or substantial portions of the Software. 33 | %%% 34 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 35 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 36 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 37 | %%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 38 | %%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 39 | %%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 40 | %%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 41 | %%%----------------------------------------------------------------------------- 42 | -module(str). 43 | 44 | % If using this module as a parse transform, we need to export the following: 45 | -export([parse_transform/2]). 46 | -export([str/1, str/2, bin/1, bin/2]). 47 | -export([reset_float_fmt/0, set_float_fmt/1, get_float_fmt/0]). 48 | 49 | %%%----------------------------------------------------------------------------- 50 | %%% External API 51 | %%%----------------------------------------------------------------------------- 52 | 53 | %% @doc Stringify an argument 54 | -spec str(term()) -> string(). 55 | str(I) when is_list(I) -> 56 | lists:flatten( 57 | try io_lib:format("~s", [I]) 58 | catch _:_ -> io_lib:format("~p", [I]) 59 | end); 60 | str(I) when is_integer(I) -> integer_to_list(I); 61 | str(I) when is_binary(I) -> binary_to_list(I); 62 | str(I) when is_float(I) -> float_to_list(I, get_float_fmt()); 63 | str(I) when is_atom(I) -> atom_to_list(I); 64 | str(I) -> 65 | lists:flatten(io_lib:format("~p", [I])). 66 | 67 | -type fmt_args() :: [ 68 | {decimals, Decimals :: 0..253} | 69 | {scientific, Decimals :: 0..249} | 70 | compact | short 71 | ]. 72 | 73 | %% @doc Stringify an argument with options passed to float_to_list/2 when 74 | %% the first argument is a float 75 | -spec str(term(), fmt_args()) -> string(). 76 | str(I, undefined) when is_float(I) -> float_to_list(I); 77 | str(I, Opts) when is_float(I) -> float_to_list(I, Opts); 78 | str(I,_Opts) -> str(I). 79 | 80 | %% @doc Stringify an argument and return as binary 81 | -spec bin(term()) -> binary(). 82 | bin(I) -> list_to_binary(str(I)). 83 | 84 | %% @doc Stringify an argument and return as binary 85 | -spec bin(term(), fmt_args()) -> binary(). 86 | bin(I, undefined) when is_float(I) -> float_to_binary(I); 87 | bin(I, Opts) when is_float(I) -> float_to_binary(I, Opts); 88 | bin(I, _) when is_binary(I) -> I; 89 | bin(I, _) when is_list(I) -> 90 | list_to_binary( 91 | try io_lib:format("~s", [I]) 92 | catch _:_ -> io_lib:format("~p", [I]) 93 | end); 94 | bin(I, _) when is_integer(I) -> integer_to_binary(I); 95 | bin(I, _) when is_atom(I) -> atom_to_binary(I); 96 | bin(I, _) -> list_to_binary(io_lib:format("~p", [I])). 97 | 98 | %% @doc Erase custom float format from the process dictionary 99 | reset_float_fmt() -> erase(float_fmt). 100 | 101 | %% @doc Store custom float format in the process dictionary 102 | %% Return previously stored format. 103 | %% Also see float_to_list/2 [http://erlang.org/doc/man/erlang.html#float_to_list-2] 104 | set_float_fmt(Opts) -> V=get(float_fmt), put(float_fmt, Opts), V. 105 | 106 | %% @doc Get custom float format from the process dictionary 107 | get_float_fmt() -> get(float_fmt). 108 | 109 | %%%----------------------------------------------------------------------------- 110 | %%% Internal functions 111 | %%%----------------------------------------------------------------------------- 112 | 113 | -record(opts, { 114 | i2l = false, 115 | b2l = false 116 | }). 117 | 118 | %% @doc Parse transform to be used by providing `{parse_transform, str}' option. 119 | parse_transform(AST, Opts) -> 120 | I2L = lists:member({d,str_i2l}, Opts), 121 | B2L = lists:member({d,str_b2l}, Opts), 122 | Tree = erl_syntax:form_list(AST), 123 | ModifiedTree = recurse(Tree, #opts{i2l=I2L, b2l=B2L}), 124 | erase(line), 125 | erl_syntax:revert_forms(ModifiedTree). 126 | 127 | %% Parse transform support 128 | recurse(Tree, Opt) -> 129 | update(case erl_syntax:subtrees(Tree) of 130 | [] -> Tree; 131 | List -> erl_syntax:update_tree(Tree, [[recurse(Subtree, Opt) || Subtree <- Group] 132 | || Group <- List]) 133 | end, Opt). 134 | 135 | syn_atom(A, Line) -> erl_syntax:set_pos(erl_syntax:atom(A), Line). 136 | syn_call(F,A) -> L=get(line), 137 | erl_syntax:set_pos( 138 | erl_syntax:application(syn_atom(F, L), A), L). 139 | syn_call(M,F,A) -> L=get(line), 140 | erl_syntax:set_pos( 141 | erl_syntax:application(syn_atom(M, L), syn_atom(F, L), A), L). 142 | 143 | update(Node, Opt) -> update2(Node, erl_syntax:type(Node), Opt). 144 | update2(Node, application, Opt) -> update3(Node, erl_syntax:application_operator(Node), Opt); 145 | update2(Node, _, _) -> Node. 146 | 147 | update3(Node, {atom, L, F}, Opt) -> update4(F, Node, L, Opt); 148 | update3(Node, _, _) -> Node. 149 | 150 | update4(Arg, Node, Line, _Opt) when Arg==str; Arg==bin -> 151 | %% Replace str(A, B) -> lists:flatten(io_lib:format(A, B)). 152 | %% str(A) -> str:str(A). 153 | put(line, Line), 154 | case erl_syntax:application_arguments(Node) of 155 | [A,B] -> 156 | %% This is a call to str(Fmt, Args). 157 | %% Replace it with: 158 | %% lists:flatten(io_libs:format(Fmt, Args) 159 | Res = syn_call(lists, flatten, [syn_call(io_lib, format, [A,B])]), 160 | case Arg of 161 | str -> Res; 162 | bin -> syn_call(erlang, list_to_binary, [Res]) 163 | end; 164 | [A] -> 165 | %% This is a call to str(Arg). 166 | %% Replace it with: 167 | %% str:str(Args) or str:bin(Args) 168 | syn_call(str, Arg, [A]); 169 | _ -> 170 | Node 171 | end; 172 | update4(I, Node, Line, _Opt) when I==throw; I==error -> 173 | %% Replace throw(A, B) -> throw(list_to_binary(io_lib:format(A, B))). 174 | %% Replace error(A, B) -> error(list_to_binary(io_lib:format(A, B))). 175 | put(line, Line), 176 | case erl_syntax:application_arguments(Node) of 177 | [A,B] -> 178 | syn_call(I, [syn_call(erlang, list_to_binary, [syn_call(io_lib, format, [A,B])])]); 179 | _ -> 180 | Node 181 | end; 182 | update4(i2l, Node, Line, #opts{i2l=true}) -> 183 | %% Replace i2l(A) -> integer_to_list(A). 184 | put(line, Line), 185 | case erl_syntax:application_arguments(Node) of 186 | [A] -> syn_call(integer_to_list, [A]); 187 | _ -> Node 188 | end; 189 | update4(b2l, Node, Line, #opts{b2l=true}) -> 190 | %% Replace b2l(A) -> binary_to_list(A). 191 | put(line, Line), 192 | case erl_syntax:application_arguments(Node) of 193 | [A] -> syn_call(binary_to_list, [A]); 194 | _ -> Node 195 | end; 196 | update4(_, Node, _, _) -> 197 | Node. 198 | -------------------------------------------------------------------------------- /test/defarg_test.erl: -------------------------------------------------------------------------------- 1 | %% vim:ts=2:sw=2:et 2 | -module(defarg_test). 3 | 4 | -compile({parse_transform, defarg}). 5 | 6 | -ifdef(TEST). 7 | -include_lib("eunit/include/eunit.hrl"). 8 | -endif. 9 | 10 | %%%----------------------------------------------------------------------------- 11 | %%% Unit Tests 12 | %%%----------------------------------------------------------------------------- 13 | 14 | -ifdef(EUNIT). 15 | 16 | defarg_test() -> 17 | ?assertEqual(3, a()), 18 | ?assertEqual(9, a(7)), 19 | ?assertEqual(10, a(6,4)), 20 | ?assertEqual(3, b()), 21 | ?assertEqual(7.0, c()), 22 | ?assertEqual(9, d()), 23 | ?assertEqual(5, d(abc, [1,2])), 24 | ok. 25 | 26 | a(A / 1, B / 2) -> 27 | A+B. 28 | 29 | b(A / #{}, B / <<>>) -> 30 | maps:get(x, A, 3) + byte_size(B). 31 | 32 | c(A / (10*2-15), B / (64 / 32)) -> 33 | A + B. 34 | 35 | d(A / undefined, B / []) -> 36 | length(atom_to_list(A)) + length(B). 37 | 38 | -endif. 39 | -------------------------------------------------------------------------------- /test/erlpipe_test.erl: -------------------------------------------------------------------------------- 1 | %% vim:ts=2:sw=2:et 2 | -module(erlpipe_test). 3 | 4 | -compile({parse_transform, erlpipe}). 5 | 6 | -ifdef(TEST). 7 | -include_lib("eunit/include/eunit.hrl"). 8 | -endif. 9 | 10 | %%%----------------------------------------------------------------------------- 11 | %%% Unit Tests 12 | %%%----------------------------------------------------------------------------- 13 | 14 | -ifdef(EUNIT). 15 | 16 | erlpipe_test() -> 17 | ?assertEqual("1\n", test1(1)), 18 | ?assertEqual("ab2", test2(10)), 19 | ?assertEqual(b, test3(3, [{1,a},{10,b}])), 20 | ?assertEqual(5.0, test4(25, 5)), 21 | ?assertEqual(8, test5()), 22 | ?assertEqual(28, test6()), 23 | ?assertEqual(7, test7()), 24 | ?assertEqual(10, test_tap()), 25 | ?assertEqual(2, [2] / (fun t/1)), 26 | ?assertEqual(2, [[1,2]] / (fun erlang:length/1)), 27 | ?assertEqual(2, [2] / t), 28 | ?assertEqual(2, [2] / fun(I) -> I end), 29 | ?assertEqual(2, [2] / (fun(I) -> I end)(_)), 30 | ?assertEqual(1.0, 10 / min(2,3) / 5.0), 31 | ?assertEqual(2.0, 10 / 5), 32 | ?assertEqual(1, [[1]] / hd), 33 | ?assertEqual(3, abc / atom_to_list / length), 34 | ?assertEqual(3, "abc" / length), 35 | ?assertEqual("abc", <<"abc">> / binary_to_list), 36 | ?assertEqual("c", [2] / lists:nthtail(_, "abc")), 37 | ?assertEqual("1,2,3", {$1,$2,$3} / tuple_to_list / [[I] || I <- _] / string:join(_, ",")), 38 | ?assertEqual("abc\n", "abc" / (_ ++ "\n")), 39 | ?assertEqual(8, "abc" / (fun(A) -> A - length(_) end)(10) / (_ + 1) ). 40 | 41 | test1(A) -> 42 | [A] / integer_to_list 43 | / list_to_integer() 44 | / element(_, {1,2}) 45 | / io_lib:format("~w\n", [_]) 46 | / lists:flatten 47 | / ttt(get(env)). 48 | 49 | test2(A) -> 50 | [A+10] 51 | / integer_to_list 52 | / lists:append(["ab", _]) 53 | / lists:split(3, _) 54 | / element(1, _). 55 | 56 | test3(A, B) -> 57 | [B ++ [{5,c}], max(A,10)] 58 | / lists:keyfind(_2, 1, _1) 59 | / element(2, _). 60 | 61 | test4(A, B) -> 62 | % To make sure the parse transform doesn't touch `A / B' expressions, 63 | % where `A' is a function call, an integer, or a float. 64 | C = begin 65 | max(A, 20) / min(B, 20) 66 | end, 67 | D = 5.0 / C, 68 | E = 5 / trunc(C), 69 | erlang:max(A, 1) / max(C, 5) * D * E. 70 | 71 | test5() -> 72 | %% I.e.: g(length([max(1,2)]), f(5, h(t(2)))). 73 | [1] / max(2) 74 | / ([2] ++ [_]) 75 | / length([_]) 76 | / ([t(2)] / f(5, [_] / h) / g). 77 | 78 | test6() -> 79 | %% I.e.: 20 + length(atom_to_list(abc)) + length("ee" ++ "efg") 80 | abc / atom_to_list 81 | / length 82 | / (20 + _ + ("ee" / (_ ++ "efg") / length)). 83 | 84 | test7() -> 85 | 6 = [I || I <- [1,2,3]] / lists:sum, 86 | L = [{a, [1]}, {b, [1]}, {c, [1]}], 87 | [1] = [I || {_, I} <- L] 88 | / lists:append 89 | / sets:from_list 90 | / sets:to_list, 91 | 6 = {1,2,3} 92 | / tuple_to_list 93 | / [I || I <- _] 94 | / lists:sum, 95 | [I || I <- [1,2,3]] / sumit(1). 96 | 97 | sumit(L, J) -> 98 | f(lists:sum(L), J). 99 | 100 | test_tap() -> 101 | [10] / max(2) 102 | / tap(fun(A) -> A+1 end) 103 | / tap(t1) 104 | / tap(fun t1/1). 105 | 106 | t(A) -> A. 107 | t1(A) -> A+1. 108 | h(I) -> I. 109 | f(I, J) -> I+J. 110 | g(I, J) -> I+J. 111 | 112 | ttt(A, _B) -> 113 | A. 114 | 115 | %fun1(A, B, C) -> [A+B, B+C]. 116 | %fun2(A, B) -> [1, 2, A+B]. 117 | %fun3(A, B, C) -> A+B+C. 118 | 119 | -endif. 120 | -------------------------------------------------------------------------------- /test/etran_test.erl: -------------------------------------------------------------------------------- 1 | -module(etran_test). 2 | 3 | -compile({parse_transform, etran}). 4 | 5 | -ifdef(TEST). 6 | -include_lib("eunit/include/eunit.hrl"). 7 | -endif. 8 | 9 | %%%----------------------------------------------------------------------------- 10 | %%% Unit Tests 11 | %%%----------------------------------------------------------------------------- 12 | 13 | -ifdef(EUNIT). 14 | 15 | etran_test() -> 16 | ?assertEqual(6, [S+I || S = 0, I <- [1,2,3]]), 17 | ?assertEqual([{1,10}], [{Idx, I} || Idx, I <- [10]]), 18 | ?assertEqual(30, a()), 19 | ?assertEqual(3, abc / atom_to_list / length), 20 | ?assertEqual(error, iif(1 == length([1,2]), ok, error)). 21 | 22 | a(A / 10, B / 20) -> 23 | A+B. 24 | 25 | 26 | -endif. 27 | -------------------------------------------------------------------------------- /test/iif_test.erl: -------------------------------------------------------------------------------- 1 | %% vim:ts=2:sw=2:et 2 | -module(iif_test). 3 | 4 | -compile({parse_transform, iif}). 5 | 6 | -ifdef(TEST). 7 | -include_lib("eunit/include/eunit.hrl"). 8 | -endif. 9 | 10 | %%%----------------------------------------------------------------------------- 11 | %%% Unit Tests 12 | %%%----------------------------------------------------------------------------- 13 | 14 | -ifdef(EUNIT). 15 | 16 | iif_test() -> 17 | A = is_tuple(erlang:timestamp()), B = not A, 18 | ?assertEqual(ok, iif(A, ok)), 19 | ?assertEqual(undefined, iif(B, ok)), 20 | ?assertEqual(ok, iif(A, ok, error)), 21 | ?assertEqual(error, iif(B, ok, error)), 22 | ?assertEqual(ok, iif(is_tuple(erlang:timestamp()), ok, error)), 23 | ?assertEqual(ok, iif(1, 1, ok, error)), 24 | ?assertEqual(error, iif(1, 2, ok, error)). 25 | 26 | nvl_test() -> 27 | ?assertEqual(ok, nvl(false, ok)), 28 | ?assertEqual(true, nvl(true, ok)), 29 | ?assertEqual(1, nvl(1, ok)), 30 | ?assertEqual(error, nvl(true, ok, error)), 31 | ?assertEqual(ok, nvl(false, ok, error)), 32 | ?assertEqual(error, nvl(1, ok, error)). 33 | 34 | -endif. 35 | -------------------------------------------------------------------------------- /test/listcomp_test.erl: -------------------------------------------------------------------------------- 1 | %% vim:ts=2:sw=2:et 2 | -module(listcomp_test). 3 | 4 | -compile({parse_transform, listcomp}). 5 | 6 | -ifdef(TEST). 7 | -include_lib("eunit/include/eunit.hrl"). 8 | -endif. 9 | 10 | %%%----------------------------------------------------------------------------- 11 | %%% Unit Tests 12 | %%%----------------------------------------------------------------------------- 13 | 14 | -ifdef(EUNIT). 15 | 16 | fold_test() -> 17 | ?assertEqual(6, [S+I || S = 0, I <- [1,2,3]]), 18 | ?assertEqual(6, [S+I || S = 0, {I,_} <- [{1,a},{2,b},{3,c}]]), 19 | ?assertEqual(4, [S+I || S = 0, I <- [1,2,3], I /= 2]), 20 | ?assertEqual(9, [S+I+J || S = 0, I <- [1,2], J <- [3,4], I /= 2]), 21 | ok. 22 | 23 | indexed_fold_test() -> 24 | ?assertEqual([{1,10},{2,20},{3,30}], [{Idx, I} || Idx, I <- [10,20,30]]), 25 | ?assertEqual(140, [do1(Idx, I, S) || Idx, S=0, I <- [10,20,30]]), 26 | ok. 27 | 28 | index_test() -> 29 | ?assertEqual([{1,10},{2,20},{3,30}], [{Idx,I} || Idx, {req, FF} <- [{req, [10,20,30]}], I <- FF]), 30 | ok. 31 | 32 | do1(Idx, I, S) -> 33 | S + Idx*I. 34 | 35 | foldlr_test() -> 36 | ?assertEqual([3,1], listcomp:foldl(fun(V, I, S) -> 37 | if (I rem 2 == 0) -> S; 38 | true -> [V|S] 39 | end 40 | end, [], [1,2,3,4])), 41 | ?assertEqual([1,3], listcomp:foldr(fun(V, I, S) -> 42 | if (I rem 2 == 0) -> S; 43 | true -> [V|S] 44 | end 45 | end, [], [1,2,3,4])), 46 | ok. 47 | 48 | -endif. 49 | -------------------------------------------------------------------------------- /test/str_test.erl: -------------------------------------------------------------------------------- 1 | %% vim:ts=2:sw=2:et 2 | -module(str_test). 3 | 4 | -compile({parse_transform, str}). 5 | 6 | -ifdef(TEST). 7 | -include_lib("eunit/include/eunit.hrl"). 8 | -endif. 9 | 10 | %%%----------------------------------------------------------------------------- 11 | %%% Unit Tests 12 | %%%----------------------------------------------------------------------------- 13 | 14 | -ifdef(EUNIT). 15 | 16 | str_test() -> 17 | str:set_float_fmt([{decimals, 2}]), 18 | ?assertEqual("Test: 1, ok", str("Test: ~w, ~s", [1, "ok"])), 19 | ?assertEqual("1", str(1)), 20 | ?assertEqual("1.00", str(1.0)), 21 | ?assertEqual("abc", str("abc")), 22 | ?assertEqual("abc", str(<<"abc">>)), 23 | ?assertEqual("abc", str(abc)), 24 | ?assertEqual("abc", str("abc")), 25 | ?assertEqual("[abc,1,\"e\"]", str([abc, 1, "e"])), 26 | str:reset_float_fmt(), 27 | 28 | ?assertEqual("123", i2l(123)), 29 | ?assertEqual("1", i2l(1)), 30 | ?assertEqual("1", b2l(<<"1">>)), 31 | ?assertEqual("abc", str(<<"abc">>)), 32 | ?assertEqual("1", str(1)), 33 | ?assertEqual(<<"abc">>, bin(<<"abc">>)), 34 | ?assertEqual(<<"1">>, bin(1)), 35 | str:set_float_fmt([{decimals, 2}]), 36 | ?assertEqual("1.00", str(1.0)), 37 | ?assertEqual(<<"1.00">>, bin(1.0)), 38 | str:reset_float_fmt(), 39 | ?assertEqual(<<"{a,1}">>, bin({a, 1})). 40 | 41 | throw_test() -> 42 | ?assertEqual(<<"Test: 1">>, try throw("Test: ~w", [1]) catch throw:E -> E end), 43 | ?assertEqual(ok, try throw(ok) catch throw:E -> E end). 44 | 45 | error_test() -> 46 | ?assertEqual(<<"Test: 1">>, try error("Test: ~w", [1]) catch error:E -> E end), 47 | ?assertEqual(ok, try error(ok) catch error:E -> E end). 48 | 49 | -endif. 50 | --------------------------------------------------------------------------------