├── .build.yml ├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .gitmodules ├── .luacov ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── api.md ├── bootstrap ├── aot.lua └── fennel.lua ├── build └── manfilter.fnl ├── changelog.md ├── from-clojure.md ├── lua-primer.md ├── macros.md ├── man ├── man1 │ └── fennel.1 ├── man3 │ └── fennel-api.3 ├── man5 │ └── fennel-reference.5 └── man7 │ └── fennel-tutorial.7 ├── rationale.md ├── reference.md ├── security.md ├── setup.md ├── src ├── fennel.fnl ├── fennel │ ├── binary.fnl │ ├── compiler.fnl │ ├── friend.fnl │ ├── macros.fnl │ ├── match.fnl │ ├── parser.fnl │ ├── repl.fnl │ ├── specials.fnl │ ├── utils.fnl │ └── view.fnl ├── launcher.fnl └── linter.fnl ├── style.md ├── test ├── api.fnl ├── bad │ ├── all.sh │ ├── call-literal.fnl │ ├── expected-binding.fnl │ ├── expected-body.fnl │ ├── expected-even-bindings.fnl │ ├── expected-function.fnl │ ├── expected-local.fnl │ ├── expected-parameters.fnl │ ├── expected-rest.fnl │ ├── expected-symbol-parameter.fnl │ ├── global-alias.fnl │ ├── global-conflict.fnl │ ├── illegal-character.fnl │ ├── macro-bind.fnl │ ├── macro-no-return-table.fnl │ ├── mismatched-closing.fnl │ ├── multisym-digit.fnl │ ├── multisym-last.fnl │ ├── multisym-malformed.fnl │ ├── multisym-method.fnl │ ├── no-whitespace-before-open.fnl │ ├── numeric-token.fnl │ ├── odd-table.fnl │ ├── only-compile-time.fnl │ ├── set-local.fnl │ ├── special-shadow.fnl │ ├── unable-to-bind-for.fnl │ ├── unable-to-bind.fnl │ ├── unexpected-close-top.fnl │ ├── unexpected-vararg.fnl │ ├── unknown-global.fnl │ ├── unused.fnl │ └── vararg-not-last.fnl ├── bit.fnl ├── cli.fnl ├── core.fnl ├── failures.fnl ├── faith.fnl ├── fennelview.fnl ├── fuzz-string.fnl ├── fuzz.fnl ├── generate.fnl ├── indirect-macro.fnl ├── init.lua ├── irc.lua ├── linter.fnl ├── loops.fnl ├── luabad.lua ├── luamod.lua ├── macro.fnl ├── macros.fnl ├── mangling.fnl ├── misc.fnl ├── mod │ ├── bar.fnl │ ├── baz.fnl │ ├── foo.fnl │ ├── foo2.fnl │ ├── foo3.fnl │ ├── foo4.fnl │ ├── foo5.fnl │ ├── foo6-2.fnl │ ├── foo6.fnl │ ├── foo7.fnl │ ├── macroed.fnlm │ ├── nested-2 │ │ ├── mod1.fnl │ │ └── mod2.fnl │ ├── nested │ │ ├── mod1.fnl │ │ └── mod2.fnl │ ├── quux.lua │ ├── reverse.lua │ ├── splice.fnl │ └── tracer.fnl ├── other-macros │ └── init-macros.fnl ├── parser.fnl ├── plugin │ └── lua-plugin.lua ├── quoting.fnl ├── relative-chained-mac-mod-mac.fnl ├── relative-chained-mac-mod-mac │ ├── mac-head.fnl │ ├── mac-tail.fnl │ └── mod-mid.fnl ├── relative-filename.fnl ├── relative.fnl ├── relative │ └── macros.fnl ├── repl.fnl ├── searcher.fnl └── sourcemap.fnl ├── tutorial.md └── values.md /.build.yml: -------------------------------------------------------------------------------- 1 | # -*- js -*- 2 | { 3 | "image": "debian/stable", 4 | "packages": ["git", "make", "netcat-openbsd", 5 | "luajit2", "lua5.1", "lua5.2", "lua5.3", "lua5.4"], 6 | "sources": ["https://git.sr.ht/~technomancy/fennel"], 7 | "tasks": [{"build":"IRC_CHANNEL=\"#fennel\" make -C fennel ci"}], 8 | } 9 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # -*- js -*- 2 | { 3 | "version": "2.1", 4 | 5 | "jobs": { 6 | "build": { 7 | "docker": [{"image": "debian:stable"}], 8 | "steps": [ 9 | "checkout", 10 | {"run": "apt-get update -qq"}, 11 | {"run": "apt-get install -qq cloc make git luajit2 lua5.1 lua5.2 lua5.3 lua5.4"}, 12 | {"run": "make ci"}, 13 | {"run": "git diff --quiet"}, 14 | ] 15 | }, 16 | "windows": { 17 | "executor": "windows/default", 18 | "steps": [ 19 | "checkout", 20 | {"run": "choco install -y lua53"}, 21 | {"run": "choco install -y make"}, 22 | {"run": "make test LUA=lua53"} 23 | ] 24 | } 25 | }, 26 | 27 | "workflows": { 28 | "version": 2, 29 | "all": { 30 | "jobs": ["build", "windows"] 31 | } 32 | }, 33 | 34 | "orbs": { 35 | "windows": "circleci/windows@2.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Cross-editor formatting rules 2 | # see: https://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset=utf-8 8 | end_of_line=lf 9 | insert_final_newline=true 10 | trim_trailing_whitespace=true 11 | 12 | [*.lua] 13 | indent_size=4 14 | indent_style=space 15 | 16 | [*.md] 17 | # In markdown, two trailing spaces at EOL guarantees a line break in the output 18 | trim_trailing_whitespace=false 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /fennel 2 | /fennel.lua 3 | /minifennel.lua 4 | /downloads 5 | /scratch 6 | /scratch.fnl 7 | /chroot 8 | /bootstrap/view.lua 9 | /bootstrap/macros.lua 10 | /bootstrap/match.lua 11 | /test/faith.lua 12 | 13 | # items related to manpage generation 14 | /build/manfilter.lua 15 | 16 | /lua-5.* 17 | /LuaJIT-* 18 | 19 | # Created by https://www.gitignore.io/api/lua 20 | 21 | # from static compilation 22 | /*_binary.c 23 | /fennel-bin 24 | /fennel-arm32 25 | /fennel-bin-luajit 26 | 27 | ### Lua ### 28 | # Compiled Lua sources 29 | luac.out 30 | 31 | # luarocks build files 32 | *.src.rock 33 | *.zip 34 | *.tar.gz 35 | 36 | # Object files 37 | *.o 38 | *.os 39 | *.ko 40 | *.obj 41 | *.elf 42 | 43 | # Precompiled Headers 44 | *.gch 45 | *.pch 46 | 47 | # Libraries 48 | *.lib 49 | *.a 50 | *.la 51 | *.lo 52 | *.def 53 | *.exp 54 | 55 | # Shared objects (inc. Windows DLLs) 56 | *.dll 57 | *.so 58 | *.so.* 59 | *.dylib 60 | 61 | # Executables 62 | *.exe 63 | *.out 64 | *.app 65 | *.i*86 66 | *x86_64 67 | *.hex 68 | 69 | # Tooling + transient metadata 70 | *.swp 71 | tags 72 | **/*asc 73 | 74 | # Misc 75 | /wiki 76 | /.dir-locals-2.el 77 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lua"] 2 | path = lua 3 | url = https://salsa.debian.org/lua-team/lua5.4 4 | [submodule "luajit"] 5 | path = luajit 6 | url = https://salsa.debian.org/lua-team/luajit2 7 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | -- setting default behaviors for luacov. For documentation on the options, 2 | -- see https://keplerproject.github.io/luacov/doc/modules/luacov.defaults.html 3 | 4 | return { 5 | runreport = true, 6 | } 7 | 8 | -- vim: ft=lua 9 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Fennel Code of Conduct 2 | 3 | ## When Something Happens 4 | 5 | If you see a Code of Conduct violation, follow these steps: 6 | 7 | 1. Let the person know that what they did is not appropriate and ask them to stop. 8 | 2. That person should immediately stop the behavior and correct the issue. 9 | 3. If this doesn’t happen, or if you're uncomfortable speaking up, [contact admins](#contacting-admins). 10 | 4. As soon as available, an admin will identify themselves and take [further action (see below)](#further-enforcement), starting with a warning, then temporary deactivation, then long-term deactivation. 11 | 12 | When reporting, please include any relevant details, links, screenshots, context, or other information that may be used to better understand and resolve the situation. 13 | 14 | **The Admin team will prioritize the well-being and comfort of the recipients of the violation over the comfort of the violator.** See [some examples below](#enforcement-examples). 15 | 16 | ## Our Pledge 17 | 18 | In the interest of fostering an open and welcoming environment, we as members of the Fennel community pledge to making participation in our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, technical preferences, nationality, personal appearance, race, religion, or sexual identity and orientation. 19 | 20 | ## Our Standards 21 | 22 | Examples of behavior that contributes to creating a positive environment include: 23 | 24 | * Using welcoming and inclusive language. 25 | * Being respectful of differing viewpoints and experiences. 26 | * Gracefully accepting constructive feedback. 27 | * Focusing on what is best for the community. 28 | * Showing empathy and kindness towards other community members. 29 | 30 | Examples of unacceptable behavior by participants include: 31 | 32 | * The use of sexualized language or imagery and unwelcome sexual attention or advances, including when simulated online. 33 | * Trolling, insulting/derogatory comments, and personal or political attacks. 34 | * Casual mention of slavery or indentured servitude and/or false comparisons of one's occupation or situation to slavery. Please consider using or asking about alternate terminology when referring to such metaphors in technology. 35 | * Making light of/making mocking comments about trigger warnings and content warnings. 36 | * Public or private harassment, deliberate intimidation, or threats. 37 | * Publishing others' private information, such as a physical or electronic address, without explicit permission. This includes any sort of "outing" of any aspect of someone's identity without their consent. 38 | * Publishing screenshots or quotes without all quoted users' *explicit* consent. 39 | * Publishing of non-harassing private communication. 40 | * Any of the above even when [presented as "ironic" or "joking"](https://en.wikipedia.org/wiki/Hipster_racism). 41 | * Any attempt to present "reverse-ism" versions of the above as violations. Examples of reverse-isms are "reverse racism", "reverse sexism", "heterophobia", and "cisphobia". 42 | * Unsolicited explanations under the assumption that someone doesn't already know it. Ask before you teach! Don't assume what people's knowledge gaps are. 43 | * [Feigning or exaggerating surprise](https://www.recurse.com/manual#no-feigned-surprise) when someone admits to not knowing something. 44 | * "[Well-actuallies](https://www.recurse.com/manual#no-well-actuallys)" 45 | * Other conduct which could reasonably be considered inappropriate in a professional or community setting. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies both within community spaces and in other spaces involving the community. This includes the Fennel Email list, #fennel IRC channel on Libera Chat and Matrix, private email communications in the context of the community, FennelConf and any other events where members of the community are participating, as well as adjacent communities and venues affecting the community's members. 50 | 51 | Depending on the violation, the admins may decide that violations of this code of conduct that have happened outside of the scope of the community may deem an individual unwelcome, and take appropriate action to maintain the comfort and safety of its members. 52 | 53 | This Code of Conduct is detailed for the purpose of removing ambiguity, not for the sake of strictness. It is the sincere hope of admins that it helps foster mutual understanding, and the creation of a space where everyone can participate in a way relevant to the project itself, without things going horribly due to accidental/well-intentioned toe stepping. Please be kind to one another! 54 | 55 | ## Admin Enforcement Process 56 | 57 | Once the admins get involved, they will follow a documented series of steps and do their best to preserve the well-being of community members. This section covers actual concrete steps. 58 | 59 | ### Contacting Admins 60 | 61 | You may get in touch with the Fennel admin team through any of the following methods: 62 | 63 | * In the `#fennel` IRC channel on libera.chat 64 | * In the `#fennel:matrix.org` channel, which is bridged to libera 65 | * In a private message on IRC 66 | * In a direct message or mention on the Fediverse 67 | * Over email 68 | 69 | ### Further Enforcement 70 | 71 | If you've already followed the [initial enforcement steps](#enforcement), these are the steps admins will take for further enforcement, as needed: 72 | 73 | 1. Repeat the request to stop. 74 | 2. If the person doubles down, they will be removed from the channel and given an official warning. 75 | 3. If the behavior continues or is repeated later, the person will be deactivated for 24 hours. 76 | 4. If the behavior continues or is repeated after the temporary deactivation, a long-term (6-12mo) deactivation will be used. 77 | 78 | On top of this, admins may remove any offending messages, images, contributions, etc, as they deem necessary. 79 | 80 | Admins reserve full rights to skip any of these steps, at their discretion, if the violation is considered to be a serious and/or immediate threat to the health and well-being of members of the community. These include any threats, serious physical or verbal attacks, and other such behavior that would be completely unacceptable in any social setting that puts our members at risk. 81 | 82 | Members expelled from events or venues with any sort of paid attendance will not be refunded. 83 | 84 | ### Who Watches the Watchers? 85 | 86 | Admins and other leaders who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the community's leadership. These may include anything from removal from the admin team to a permanent ban from the community. 87 | 88 | ### Enforcement Examples 89 | 90 | #### The Best Case 91 | 92 | The vast majority of situations work out like this, in our experience. This interaction is common, and generally positive. 93 | 94 | > Alex: "Yeah I used X and it was really retarded!" 95 | 96 | > Patt: "Hey, could you not use that word? What about 'ridiculous' instead?" 97 | 98 | > Alex: "oh sorry, sure. will be more careful in the future." 99 | 100 | #### The Admin Case 101 | 102 | Sometimes, though, you need to get admins involved. Admins will do their best to resolve conflicts, but people who were harmed by something **will take priority**. 103 | 104 | > Patt: "Honestly, sometimes I just really hate using $language and anyone who uses it probably sucks at their job." 105 | 106 | > Alex: "Whoa there, could you dial it back a bit? There's a CoC thing about attacking folks' tech use like that." 107 | 108 | > Patt: "I'm not attacking anyone, are you deaf?" 109 | 110 | > Alex: *DMs admin* "hey uh. Can someone look at #fennel? Patt is getting a bit aggro. I tried to nudge them about it, but nope." 111 | 112 | > MxAdmin1: "Hey Patt, admin here. Could you tone it down? This sort of attack is really not okay in this space." 113 | 114 | > Patt: "Leave me alone I haven't said anything bad wtf is wrong with you." 115 | 116 | > MxAdmin1: *removes patt* *DMs patt* "I mean it. Please refer to the CoC in the repository if you have questions, but you can consider this an actual warning. I'd appreciate it if you reworded your messages in #fennel, since they made folks there uncomfortable. Let's try and be kind, yeah?" 117 | 118 | > Patt: *Replies to DM* "@mxadmin1 Okay sorry. I'm just frustrated and I'm kinda burnt out and I guess I got carried away. I'll DM Alex a note apologizing. Sorry for the trouble." 119 | 120 | > MxAdmin1: *Replies to DM* "@patt Thanks for that. I hear you on the stress. Burnout sucks :/ Have a good one!" 121 | 122 | #### The Nope Case 123 | 124 | > PepeTheFrog: "Hi, I am a literal actual nazi and I think white supremacists are quite fashionable." 125 | 126 | > Patt: "NOOOOPE. OH NOPE NOPE." 127 | 128 | > Alex: "JFC NO. NOPE. /msg MxAdmin1 Nope nope nope" 129 | 130 | > MxAdmin1: "/kick PepeTheFrog" 131 | 132 | > PepeTheFrog has been kicked. 133 | 134 | ## Attribution 135 | 136 | This Code of Conduct is adapted from the [Package Community Code of Conduct](http://package.community/code-of-conduct), which was adapted from the [WeAllJS Code of Conduct](https://wealljs.org/code-of-conduct), 137 | itself adapted from [Contributor Covenant](https://contributor-covenant.org) version 1.4 (available at 138 | [contributor-covenant.org/version/1/4](https://contributor-covenant.org/version/1/4)) and the LGBTQ in 139 | Technology Slack [Code of Conduct](https://lgbtq.technology/coc.html). 140 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Guidelines for contributing to Fennel 2 | 3 | > True leaders 4 | > are hardly known to their followers. 5 | > Next after them are the leaders 6 | > the people know and admire; 7 | > after them, those they fear; 8 | > after them, those they despise. 9 | > 10 | > To give no trust 11 | > is to get no trust. 12 | > 13 | > When the work's done right, 14 | > with no fuss or boasting, 15 | > ordinary people say, 16 | > Oh, we did it. 17 | > 18 | > - Tao Te Ching chapter 17, translated by Ursula K. Le Guin 19 | 20 | ## Reporting bugs 21 | 22 | * Check past and current issues to see if your problem has been run into before. 23 | Take a look at the [issue tracker][3], the [mailing list][2], and 24 | the [old issue tracker][6]. 25 | * If you can't find a past issue for your problem, you should open a new issue. 26 | If there is a closed issue that is relevant, make sure to reference it. 27 | * As with any project, include a comprehensive description of the problem and 28 | instructions on how to reproduce it. If it is a compiler or language bug, 29 | please try to include a minimal example. This means don't post all 800 lines 30 | of code from your project, but spend some time distilling the problem to just 31 | the relevant code. 32 | 33 | ## Codebase Organization 34 | 35 | The `fennel` module is the fundamental entry point; it provides the entire 36 | public API for Fennel when it's used inside another program. All other modules 37 | except `fennel.view` are considered compiler internals and do not have a 38 | guaranteed stable API. 39 | 40 | * `src/fennel.fnl`: returned when Fennel is embedded programmatically 41 | * `src/launcher.fnl`: handles being launched from the command line 42 | * `src/fennel/repl.fnl`: provides interactive development context 43 | 44 | The core modules implement the text->AST->Lua pipeline. The AST used 45 | by the compiler is the exact same AST that is exposed to macros. 46 | 47 | * `src/fennel/parser.fnl`: turns text of code into an AST 48 | * `src/fennel/compiler.fnl`: turns AST into Lua output 49 | * `src/fennel/specials.fnl`: built-in fundamental language constructs 50 | * `src/fennel/macros.fnl`: built-in language constructs that use fundamentals 51 | * `src/fennel/match.fnl`: pattern matching macro implementations 52 | * `src/fennel/utils.fnl`: definitions of core AST types and helper functions 53 | 54 | Finally there are a few miscellaneous modules: 55 | 56 | * `src/fennel/friend.fnl`: emits friendly messages from compiler/parser errors 57 | * `src/fennel/binary.fnl`: produces binary standalone executables 58 | * `src/fennel/view.fnl`: turn Fennel data structures into printable strings 59 | 60 | ### Bootstrapping 61 | 62 | Fennel is written in Fennel. In order to get around the chicken-and-egg 63 | problem, we include an older version of the compiler (written in Lua) 64 | that's used to compile the new version (written in Fennel). 65 | 66 | * `bootstrap/fennel.lua`: version 0.4.x of the compiler library 67 | * `bootstrap/aot.lua`: short shim which wraps the library to do AOT 68 | 69 | Not all changes need to be backported to the bootstrap compiler, but 70 | new macros generally should be. 71 | 72 | The file `src/fennel/macros.fnl` where the built-in macros are defined 73 | is evaluated by the compiler in `src/`, not by the bootstrap compiler. 74 | This means that you cannot use any macros here; for instance it's 75 | necessary to use `if` even in cases where `when` would make more sense. 76 | 77 | The file `src/fennel/match.fnl` contains the pattern matching macros; 78 | because of their complexity they are broken out so that they can use the rest 79 | of the macros in their implementation. 80 | 81 | ## Deciding to make a Change 82 | 83 | Before considering making a change to Fennel, please familiarize yourself 84 | with [the Values of Fennel](values.md). 85 | 86 | Fennel has made incompatible changes in the past, but at this point in its 87 | evolution we are committed to backwards compatibility. A change which breaks 88 | existing programs will only be considered if it fixes a security vulnerability. 89 | 90 | Fennel follows Lua's lead in being a language with a very small conceptual 91 | footprint. Being built on Lua, Fennel is necessarily larger than Lua, but not 92 | by a lot. We have a high bar for adding new features to the language. Once you 93 | have identified a problem and have sketched out a potential solution there are 94 | four main questions to consider: 95 | 96 | * How common is the problem? 97 | * How bad is the workaround you must employ without the proposed solution? 98 | * How much code does the proposed solution involve? 99 | * How much mental overhead does the proposed solution introduce? 100 | 101 | Let's look at some examples. 102 | 103 | The `match` macro is quite large, both in terms of its implementation and its 104 | meaning; it is by far the biggest addition to the semantics of Fennel for 105 | which a comparative construct does not exist in Lua. Pattern matching in 106 | general can be thought of as a composition of conditions and destructuring, 107 | so its addition is not as big in Fennel (where conditions and destructuring 108 | both already exist a la carte) as it would be in a language which did not 109 | already have destructuring. But weighing this cost against the benefits we note 110 | that `match` is applicable to a multitude of situations and that rewriting 111 | the code to avoid it results in ugly code. 112 | 113 | Adding `icollect` was thoroughly merited in that it is needed very frequently, 114 | and the alternative is tedious. When considering the conceptual footprint, we 115 | note that `icollect` parallels the existing `each` construct closely; the 116 | only difference being that the body of the macro is used to construct a 117 | sequential table instead of being discarded. So the cost/benefit ratio is 118 | great. The `collect` macro, on the other hand, is used much more 119 | infrequently. But it's also an even smaller change; given that `icollect` 120 | exists, it's fairly obvious how a parallel key/value-based variant would 121 | work. 122 | 123 | Note that the above only describes the process for language-level features. 124 | There are other changes which affect (say) the compiler or the repl but do not 125 | affect the language itself; the dynamic for making those changes is different 126 | and the bar (other than that of backwards-compatibility) is not quite so high. 127 | An addition to the language is a cost that everyone reading and writing Fennel 128 | code from here on out will have to pay; an addition to the API is not. 129 | 130 | ## Contributing Changes 131 | 132 | If you want to contribute code to the project, please send patches to the 133 | [mailing list][4]. Note that you do not need to subscribe to the mailing list 134 | in order to post to it. When sending patches to the mailing list, it's usually 135 | nicer to squash everything down to a single commit so that it will be sent as 136 | a single email rather than a series of messages, unless there really are two 137 | relatively unrelated changes. If you like to use [git send-email][1] you can, 138 | but since its usability is not very good, you can also just attach your patch 139 | to your message if you prefer. Running `git format-patch HEAD~` will write 140 | your most recent change to a `.patch` file you can attach. 141 | 142 | We also accept code contributions on the [GitHub mirror][5] if you prefer not 143 | to use email. For smaller changes that are unlikely to require back-and-forth 144 | discussion, you can also push your changes to a branch on a public git remote 145 | hosted anywhere you like and ask someone on IRC/Matrix or the mailing list to 146 | take a look. 147 | 148 | Please note that it is **ethically unacceptable** to submit patches (to 149 | this project or any other) which you did not author yourself without 150 | giving clear attribution to the original author. Note that this 151 | includes submitting changes generated by most so-called "artificial 152 | intelligence" language models as these systems make it impossible to even 153 | identify (much less credit) the original author. 154 | 155 | In order to get CI to automatically run your patches, they will need to have 156 | `[PATCH fennel]` in the subject. You can configure git to do this automatically: 157 | 158 | git config format.subjectPrefix 'PATCH fennel' 159 | 160 | For large changes, please discuss it first either on the mailing list, 161 | IRC/Matrix channel, or in the issue tracker before sinking time and effort into 162 | something that may not be able to get merged. 163 | 164 | * Branch off the `main` branch. The contents of this branch should be 165 | the same on Sourcehut as they are on the Github mirror. But make 166 | sure that `main` on your copy of the repo matches upstream. 167 | * Write a detailed description of the changes in the commit message, including 168 | motivation for the change and alternatives which were considered but decided 169 | against. One-line commit messages are only appropriate for trivial changes. 170 | * Please include tests if at all possible. You can run tests with `make test`. 171 | Fennel's tests use the [faith](https://git.sr.ht/~technomancy/faith) 172 | library; see the docs there and follow the conventions in existing tests. 173 | * For smaller changes you can just test against a single version of Lua (with 174 | `make test`) and rely on the CI suite to run the rest, but for larger 175 | changes please make sure that your changes will work on Lua versions 5.1, 5.2, 176 | 5.3, 5.4, and LuaJIT. Making fennel require LuaJIT or 5.2+ specific 177 | features is not going to fly. In general, this means target Lua 178 | 5.1, but provide shims for where functionality is different in newer Lua 179 | versions. Running `make testall` will test against all supported versions, 180 | assuming they're installed. 181 | * Be consistent with the style of the project. Please try to code moderately 182 | tersely; code is a liability, so the less of it there is, the better. 183 | * For user-visible changes, include a description of the change in 184 | `changelog.md`. Changes that affect the compiler API should update `api.md` 185 | while changes to the built-in forms will usually need to update 186 | `reference.md` to reflect the new behavior. 187 | * The [fennel-ls][7] analysis gives a lot of false positives on the 188 | compiler, but it can be helpful to use it to ensure your own changes 189 | don't introduce new warnings. 190 | * Please be patient if it takes a long time to get feedback on your change; 191 | there are very few people who review patches, and no one works on Fennel 192 | for their day job. 193 | 194 | [1]: https://man.sr.ht/git.sr.ht/send-email.md 195 | [2]: https://lists.sr.ht/%7Etechnomancy/fennel 196 | [3]: https://todo.sr.ht/~technomancy/fennel 197 | [4]: mailto:~technomancy/fennel@lists.sr.ht 198 | [5]: https://github.com/bakpakin/Fennel 199 | [6]: https://github.com/bakpakin/Fennel/issues 200 | [7]: https://git.sr.ht/~xerool/fennel-ls 201 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LUA ?= lua 2 | LUA_VERSION ?= $(shell $(LUA) -e 'v=_VERSION:gsub("^Lua *","");print(v)') 3 | DESTDIR ?= 4 | PREFIX ?= /usr/local 5 | BIN_DIR ?= $(PREFIX)/bin 6 | LUA_LIB_DIR ?= $(PREFIX)/share/lua/$(LUA_VERSION) 7 | MAN_DIR ?= $(PREFIX)/share 8 | 9 | MAKEFLAGS += --no-print-directory 10 | 11 | CORE_SRC=src/fennel.fnl src/fennel/parser.fnl src/fennel/specials.fnl \ 12 | src/fennel/utils.fnl src/fennel/compiler.fnl src/fennel/macros.fnl \ 13 | src/fennel/match.fnl 14 | 15 | LIB_SRC=$(CORE_SRC) src/fennel/friend.fnl src/fennel/view.fnl src/fennel/repl.fnl 16 | 17 | SRC=$(LIB_SRC) src/launcher.fnl src/fennel/binary.fnl 18 | 19 | PRECOMPILED=bootstrap/view.lua bootstrap/macros.lua bootstrap/match.lua 20 | 21 | MAN_PANDOC = pandoc -f gfm -t man -s --lua-filter=build/manfilter.lua \ 22 | --metadata author="Fennel Maintainers" \ 23 | --variable footer="fennel $(shell ./fennel -e '(. (require :fennel) :version)')" 24 | 25 | unexport NO_COLOR # this causes test failures 26 | unexport FENNEL_PATH FENNEL_MACRO_PATH # ensure isolation 27 | 28 | build: fennel fennel.lua 29 | 30 | test: fennel.lua fennel test/faith.lua 31 | @LUA_PATH=?.lua $(LUA) test/init.lua $(TESTS) 32 | @echo 33 | 34 | testall: export FNL_TESTALL=yes 35 | testall: fennel test/faith.lua # recursive make considered not really a big deal 36 | $(MAKE) test LUA=lua5.1 37 | $(MAKE) test LUA=lua5.2 38 | $(MAKE) test LUA=lua5.3 39 | $(MAKE) test LUA=lua5.4 40 | $(MAKE) test LUA=luajit 41 | 42 | fuzz: fennel ; $(MAKE) test TESTS=test.fuzz 43 | 44 | count: ; cloc $(CORE_SRC); cloc $(LIB_SRC) ; cloc $(SRC) 45 | 46 | # install https://git.sr.ht/~technomancy/fnlfmt manually for this: 47 | format: ; for f in $(SRC); do fnlfmt --fix $$f ; done 48 | 49 | # All-in-one pure-lua script: 50 | fennel: src/launcher.fnl $(SRC) bootstrap/aot.lua $(PRECOMPILED) 51 | @echo "#!/usr/bin/env $(LUA)" > $@ 52 | @echo "-- SPDX-License-Identifier: MIT" >> $@ 53 | @echo "-- SPDX-FileCopyrightText: Calvin Rose and contributors" >> $@ 54 | FENNEL_PATH=src/?.fnl $(LUA) bootstrap/aot.lua $< --require-as-include >> $@ 55 | @chmod 755 $@ 56 | 57 | # Library file 58 | fennel.lua: $(SRC) bootstrap/aot.lua $(PRECOMPILED) 59 | @echo "-- SPDX-License-Identifier: MIT" > $@ 60 | @echo "-- SPDX-FileCopyrightText: Calvin Rose and contributors" >> $@ 61 | FENNEL_PATH=src/?.fnl $(LUA) bootstrap/aot.lua $< --require-as-include >> $@ 62 | 63 | bootstrap/macros.lua: src/fennel/macros.fnl; $(LUA) bootstrap/aot.lua $< --macro > $@ 64 | bootstrap/match.lua: src/fennel/match.fnl; $(LUA) bootstrap/aot.lua $< --macro > $@ 65 | bootstrap/view.lua: src/fennel/view.fnl 66 | FENNEL_PATH=src/?.fnl $(LUA) bootstrap/aot.lua $< > $@ 67 | 68 | test/faith.lua: test/faith.fnl 69 | $(LUA) bootstrap/aot.lua $< > $@ 70 | 71 | lint: 72 | fennel-ls --lint $(SRC) 73 | 74 | ci: testall fuzz fennel 75 | 76 | clean: 77 | rm -f fennel.lua fennel fennel-bin fennel.exe \ 78 | *_binary.c luacov.* $(PRECOMPILED) \ 79 | test/faith.lua build/manfilter.lua fennel-bin-luajit 80 | $(MAKE) -C $(BIN_LUA_DIR) clean || true # this dir might not exist 81 | $(MAKE) -C $(BIN_LUAJIT_DIR) clean || true # this dir might not exist 82 | rm -f $(NATIVE_LUA_LIB) $(NATIVE_LUAJIT_LIB) 83 | 84 | coverage: fennel 85 | $(LUA) -lluacov test/init.lua 86 | @echo "generated luacov.report.out" 87 | 88 | ## Binaries 89 | 90 | BIN_LUA_DIR ?= lua 91 | BIN_LUAJIT_DIR ?= luajit 92 | NATIVE_LUA_LIB ?= $(BIN_LUA_DIR)/src/liblua.a 93 | NATIVE_LUAJIT_LIB ?= $(BIN_LUAJIT_DIR)/src/libluajit.a 94 | LUA_INCLUDE_DIR ?= $(BIN_LUA_DIR)/src 95 | LUAJIT_INCLUDE_DIR ?= $(BIN_LUAJIT_DIR)/src 96 | 97 | COMPILE_ARGS=FENNEL_PATH=src/?.fnl FENNEL_MACRO_PATH=src/?.fnl CC_OPTS=-static 98 | LUAJIT_COMPILE_ARGS=FENNEL_PATH=src/?.fnl FENNEL_MACRO_PATH=src/?.fnl 99 | 100 | $(LUA_INCLUDE_DIR): ; git submodule update --init 101 | $(LUAJIT_INCLUDE_DIR): ; git submodule update --init 102 | 103 | # Native binary for whatever platform you're currently on 104 | fennel-bin: src/launcher.fnl $(BIN_LUA_DIR)/src/lua $(NATIVE_LUA_LIB) fennel 105 | $(COMPILE_ARGS) $(BIN_LUA_DIR)/src/lua fennel \ 106 | --no-compiler-sandbox --compile-binary \ 107 | $< $@ $(NATIVE_LUA_LIB) $(LUA_INCLUDE_DIR) 108 | 109 | fennel-bin-luajit: src/launcher.fnl $(NATIVE_LUAJIT_LIB) fennel 110 | $(LUAJIT_COMPILE_ARGS) $(BIN_LUAJIT_DIR)/src/luajit fennel \ 111 | --no-compiler-sandbox --compile-binary \ 112 | $< $@ $(NATIVE_LUAJIT_LIB) $(LUAJIT_INCLUDE_DIR) 113 | 114 | $(BIN_LUA_DIR)/src/lua: $(LUA_INCLUDE_DIR) ; make -C $(BIN_LUA_DIR) 115 | $(NATIVE_LUA_LIB): $(LUA_INCLUDE_DIR) ; $(MAKE) -C $(BIN_LUA_DIR)/src liblua.a 116 | $(NATIVE_LUAJIT_LIB): $(LUAJIT_INCLUDE_DIR) 117 | $(MAKE) -C $(BIN_LUAJIT_DIR) BUILDMODE=static 118 | 119 | fennel.exe: src/launcher.fnl fennel $(LUA_INCLUDE_DIR)/liblua-mingw.a 120 | $(COMPILE_ARGS) ./fennel --no-compiler-sandbox \ 121 | --compile-binary $< fennel-bin \ 122 | $(LUA_INCLUDE_DIR)/liblua-mingw.a $(LUA_INCLUDE_DIR) 123 | mv fennel-bin.exe $@ 124 | 125 | $(BIN_LUA_DIR)/src/liblua-mingw.a: $(LUA_INCLUDE_DIR) 126 | $(MAKE) -C $(BIN_LUA_DIR)/src clean mingw CC=x86_64-w64-mingw32-gcc 127 | mv $(BIN_LUA_DIR)/src/liblua.a $@ 128 | $(MAKE) -C $(BIN_LUA_DIR)/src clean 129 | 130 | ## Install-related tasks: 131 | 132 | MAN_DOCS := man/man1/fennel.1 man/man3/fennel-api.3 man/man5/fennel-reference.5\ 133 | man/man7/fennel-tutorial.7 134 | 135 | # The empty line in maninst is necessary for it to emit distinct commands 136 | define maninst = 137 | mkdir -p $(dir $(2)) && cp $(1) $(2) 138 | 139 | endef 140 | 141 | install: fennel fennel.lua 142 | mkdir -p $(DESTDIR)$(BIN_DIR) && cp fennel $(DESTDIR)$(BIN_DIR)/ 143 | mkdir -p $(DESTDIR)$(LUA_LIB_DIR) && cp fennel.lua $(DESTDIR)$(LUA_LIB_DIR)/ 144 | $(foreach doc,$(MAN_DOCS),\ 145 | $(call maninst,$(doc),$(DESTDIR)$(MAN_DIR)/$(doc))) 146 | 147 | uninstall: 148 | rm $(DESTDIR)$(BIN_DIR)/fennel 149 | rm $(DESTDIR)$(LUA_LIB_DIR)/fennel.lua 150 | rm $(addprefix $(DESTDIR)$(MAN_DIR)/,$(MAN_DOCS)) 151 | 152 | build/manfilter.lua: build/manfilter.fnl fennel.lua fennel 153 | ./fennel --correlate --compile $< > $@ 154 | 155 | man: $(dir $(MAN_DOCS)) $(MAN_DOCS) 156 | man/man%/: ; mkdir -p $@ 157 | man/man3/fennel-%.3: %.md build/manfilter.lua 158 | $(MAN_PANDOC) $< -o $@ 159 | sed -i 's/\\f\[C\]/\\f[CR]/g' $@ # work around pandoc 2.x bug 160 | man/man5/fennel-%.5: %.md build/manfilter.lua 161 | $(MAN_PANDOC) $< -o $@ 162 | sed -i 's/\\f\[C\]/\\f[CR]/g' $@ 163 | man/man7/fennel-%.7: %.md build/manfilter.lua 164 | $(MAN_PANDOC) $< -o $@ 165 | sed -i 's/\\f\[C\]/\\f[CR]/g' $@ 166 | 167 | ## Release-related tasks: 168 | 169 | SSH_KEY ?= ~/.ssh/id_ed25519.pub 170 | 171 | test-builds: fennel test/faith.lua 172 | ./fennel --metadata --eval "(require :test.init)" 173 | $(MAKE) install PREFIX=/tmp/opt 174 | 175 | upload: fennel fennel.lua fennel-bin 176 | $(MAKE) fennel.exe CC=x86_64-w64-mingw32-gcc 177 | mkdir -p downloads/ 178 | mv fennel downloads/fennel-$(VERSION) 179 | mv fennel.lua downloads/fennel-$(VERSION).lua 180 | mv fennel-bin downloads/fennel-$(VERSION)-x86_64 181 | mv fennel.exe downloads/fennel-$(VERSION).exe 182 | gpg -ab downloads/fennel-$(VERSION) 183 | gpg -ab downloads/fennel-$(VERSION).lua 184 | gpg -ab downloads/fennel-$(VERSION)-x86_64 185 | gpg -ab downloads/fennel-$(VERSION).exe 186 | ssh-keygen -Y sign -f $(SSH_KEY) -n file downloads/fennel-$(VERSION) 187 | ssh-keygen -Y sign -f $(SSH_KEY) -n file downloads/fennel-$(VERSION).lua 188 | ssh-keygen -Y sign -f $(SSH_KEY) -n file downloads/fennel-$(VERSION)-x86_64 189 | ssh-keygen -Y sign -f $(SSH_KEY) -n file downloads/fennel-$(VERSION).exe 190 | rsync -rtAv downloads/fennel-$(VERSION)* \ 191 | fenneler@fennel-lang.org:fennel-lang.org/downloads/ 192 | 193 | release: guard-VERSION upload 194 | git tag -v $(VERSION) # created by prerelease target 195 | git push 196 | git push --tags 197 | @echo "* Update the submodule in the fennel-lang.org repository." 198 | @echo "* Announce the release on the mailing list." 199 | @echo "* Bump the version in src/fennel/utils.fnl to the next dev version." 200 | @echo "* Add a stub for the next version in changelog.md" 201 | 202 | prerelease: guard-VERSION ci test-builds 203 | @echo "Did you look for changes that need to be mentioned in help/man text?" 204 | sed -i s/$(VERSION)-dev/$(VERSION)/ src/fennel/utils.fnl 205 | $(MAKE) man 206 | grep "$(VERSION)" setup.md > /dev/null 207 | ! grep "???" changelog.md 208 | git commit -a -m "Release $(VERSION)" 209 | git tag -s $(VERSION) -m $(VERSION) 210 | 211 | guard-%: 212 | @ if [ "${${*}}" = "" ]; then \ 213 | echo "Environment variable $* not set"; \ 214 | exit 1; \ 215 | fi 216 | 217 | .PHONY: build test testall fuzz count format ci clean coverage install \ 218 | man upload prerelease release guard-VERSION test-builds lint 219 | 220 | # Steps to release a new Fennel version 221 | 222 | # The `make release` command should be run on a system with the lowest 223 | # available glibc for maximum compatibility. 224 | 225 | # 1. Check for changes which need to be mentioned in help text or man page 226 | # 2. Date `changelog.md` and update download links in `setup.md` 227 | # 3. Run `make prerelease VERSION=$VERSION` 228 | # 4. Update fennel-lang.org's fennel submodule and `make html` there 229 | # 5. Run `make release VERSION=$VERSION` 230 | # 6. Run `make upload` in fennel-lang.org. 231 | # 7. Announce on the mailing list 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fennel 2 | 3 | [Fennel][1] is a lisp that compiles to Lua. It aims to be easy to use, 4 | expressive, and has almost zero overhead compared to writing Lua directly. 5 | 6 | * *Full Lua compatibility* - You can use any function or library from Lua. 7 | * *Zero overhead* - Compiled code should be just as efficient as hand-written Lua. 8 | * *Compile-time macros* - Ship compiled code with no runtime dependency on Fennel. 9 | * *Embeddable* - Fennel is a one-file library as well as an executable. Embed it in other programs to support runtime extensibility and interactive development. 10 | 11 | At [https://fennel-lang.org][1] there's a live in-browser repl you can 12 | use without installing anything. At [https://fennel-lang.org/see][3] 13 | you can see what Lua output a given piece of Fennel compiles to, or 14 | what the equivalent Fennel for a given piece of Lua would be. 15 | 16 | ## Documentation 17 | 18 | * The [setup](setup.md) guide is a great place to start 19 | * The [tutorial](tutorial.md) teaches the basics of the language 20 | * The [rationale](rationale.md) explains the reasoning of why Fennel was created 21 | * The [reference](reference.md) describes all Fennel special forms 22 | * The [macro guide](macros.md) explains how to write macros 23 | * The [API listing](api.md) shows how to integrate Fennel into your codebase 24 | * The [style guide](style.md) gives tips on how to write clear and concise code 25 | * The [Lua primer](lua-primer.md) gives a very brief intro to Lua with 26 | pointers to further details 27 | 28 | For more examples, see [the cookbook][2] on [the wiki][7]. 29 | 30 | The [changelog](changelog.md) has a list of user-visible changes for 31 | each release. 32 | 33 | ## Example 34 | 35 | #### Hello World 36 | ```Fennel 37 | (print "hello, world!") 38 | ``` 39 | 40 | #### Fibonacci sequence 41 | ```Fennel 42 | (fn fib [n] 43 | (if (< n 2) 44 | n 45 | (+ (fib (- n 1)) (fib (- n 2))))) 46 | 47 | (print (fib 10)) 48 | ``` 49 | 50 | ## Differences from Lua 51 | 52 | * Syntax is much more regular and predictable (no statements; no operator precedence) 53 | * It's impossible to set *or read* a global by accident 54 | * Pervasive destructuring anywhere locals are introduced 55 | * Clearer syntactic distinction between sequential tables and key/value tables 56 | * Separate looping constructs for numeric loops vs iterators instead of overloading `for` 57 | * Comprehensions result in much more succinct table transformations 58 | * Opt-in mutability for local variables 59 | * Opt-in nil checks for function arguments 60 | * Pattern matching 61 | * Ability to extend the syntax with your own macros 62 | 63 | ## Differences from other lisp languages 64 | 65 | * Its VM can be embedded in other programs with only ~200kb 66 | * Access to [excellent FFI][4] 67 | * LuaJIT consistently ranks at the top of performance shootouts 68 | * Inherits aggressively simple semantics from Lua; easy to learn 69 | * Lua VM is already embedded in databases, window managers, games, etc 70 | * Low memory usage 71 | * Readable compiler output resembles input 72 | * Easy to build small (~250kb) standalone binaries 73 | * Compilation output has no runtime dependency on Fennel 74 | 75 | ## Why not Fennel? 76 | 77 | Fennel inherits the limitations of the Lua runtime, which does not offer 78 | pre-emptive multitasking or OS-level threads. Libraries for Lua work 79 | great with Fennel, but the selection of libraries is not as extensive 80 | as it is with more popular languages. While LuaJIT has excellent 81 | overall performance, purely-functional algorithms will not be as 82 | efficient as they would be on a VM with generational garbage collection. 83 | 84 | Even for cases where the Lua runtime is a good fit, Fennel might not 85 | be a good fit when end-users are expected to write their own code to 86 | extend the program, because the available documentation for learning 87 | Lua is much more readily-available than it is for Fennel. 88 | 89 | ## Resources 90 | 91 | * Join the `#fennel` IRC chat [Libera.Chat][9] 92 | * The chat is also bridged [on Matrix][10] if you prefer 93 | * The [mailing list][5] has slower-paced discussion and announcements 94 | * Report issues on the mailing list, [Sourcehut todo][11] 95 | * The mirror on [Github][12] is for people who haven't made a Sourcehut account 96 | * You can browse and edit [the Wiki][7] 97 | * View builds in Fennel's [continuous integration][8] 98 | * Community interactions are subject to the [code of conduct](CODE-OF-CONDUCT.md). 99 | 100 | ## Building Fennel from source 101 | 102 | This requires GNU Make and Lua (5.1-5.4 or LuaJIT). 103 | 104 | 1. `cd` to a directory in which you want to download Fennel, such as `~/src` 105 | 2. Run `git clone https://git.sr.ht/~technomancy/fennel` 106 | 3. Run `cd fennel` 107 | 4. Run `make fennel` to create a standalone script called `fennel` 108 | 5. Run `sudo make install` to install system-wide (or `make install 109 | PREFIX=$HOME` if `~/bin` is on your `$PATH`) 110 | 111 | If you don't have Lua already installed on your system, you can run 112 | `make fennel-bin LUA=lua/src/lua` instead to build a standalone binary 113 | that has its own internal version of Lua. This requires having a C 114 | compiler installed; normally `gcc`. 115 | 116 | See the [contributing guide](CONTRIBUTING.md) for details about how to 117 | work on the source. 118 | 119 | ## License 120 | 121 | Unless otherwise listed, all files are copyright © 2016-2024 Calvin 122 | Rose and contributors, released under the [MIT license](LICENSE). 123 | 124 | The file `test/faith.fnl` is copyright © 2009-2023 Scott Vokes, Phil 125 | Hagelberg, and contributors, released under the [MIT license](LICENSE). 126 | 127 | The file `style.txt` is copyright © 2007-2011 Taylor R. Campbell, 128 | 2021-2023 Phil Hagelberg and contributors, released under the 129 | Creative Commons Attribution-NonCommercial-ShareAlike 3.0 130 | Unported License: https://creativecommons.org/licenses/by-nc-sa/3.0/ 131 | 132 | [1]: https://fennel-lang.org 133 | [2]: https://wiki.fennel-lang.org/Cookbook 134 | [3]: https://fennel-lang.org/see 135 | [4]: http://luajit.org/ext_ffi_tutorial.html 136 | [5]: https://lists.sr.ht/%7Etechnomancy/fennel 137 | [7]: https://wiki.fennel-lang.org/ 138 | [8]: https://builds.sr.ht/~technomancy/fennel 139 | [9]: https://libera.chat 140 | [10]: https://matrix.to/#/!rnpLWzzTijEUDhhtjW:matrix.org?via=matrix.org 141 | [11]: https://todo.sr.ht/~technomancy/fennel 142 | [12]: https://github.com/bakpakin/Fennel/issues 143 | -------------------------------------------------------------------------------- /bootstrap/aot.lua: -------------------------------------------------------------------------------- 1 | -- Just a tiny shim to allow AOT 2 | -- This is only used to bootstrap the main compiler. 3 | local fennel = dofile("bootstrap/fennel.lua") 4 | 5 | local opts = { 6 | ["compiler-env"]=_G, 7 | allowedGlobals={}, 8 | useMetadata=false, 9 | filename=assert(arg[1]), 10 | } 11 | 12 | for k in pairs(_G) do table.insert(opts.allowedGlobals, k) end 13 | 14 | for i=2,#arg do 15 | if arg[i] == "--require-as-include" then opts.requireAsInclude = true end 16 | if arg[i] == "--macro" then 17 | opts.useMetadata = "utils['fennel-module'].metadata" 18 | opts.scope = "_COMPILER" 19 | opts.allowedGlobals = false 20 | end 21 | end 22 | 23 | local f = assert(io.open(opts.filename)) 24 | local compile = function() return fennel.compileString(f:read("*a"), opts) end 25 | local ok, val = xpcall(compile, fennel.traceback) 26 | 27 | if(ok) then 28 | print(val) 29 | else 30 | io.stderr:write(val) 31 | io.stderr:write("\n") 32 | os.exit(1) 33 | end 34 | -------------------------------------------------------------------------------- /build/manfilter.fnl: -------------------------------------------------------------------------------- 1 | (local {:text pdtext :path pdpath :utils pdutils &as pd} (require :pandoc)) 2 | 3 | (fn d [msg] 4 | (io.stderr:write (: "%s\n" :format msg)) 5 | nil) 6 | 7 | (local (basename ext) (-?> PANDOC_STATE.output_file 8 | (pdpath.filename) 9 | (pdpath.split_extension))) 10 | 11 | ;; nils left in for documentation purposes - they'll be set later 12 | (local new-meta {:date (os.date "%Y-%m-%d") 13 | :section (: (assert ext "failed to detect man section from extension") 14 | :match "^%.([0-9])$") 15 | :header nil 16 | :title nil}) 17 | 18 | (fn get-meta-header-and-title [meta] 19 | ;; saving off values for later manipulation 20 | (when meta.header (set new-meta.header meta.header)) 21 | (if meta.title (set new-meta.title meta.title) 22 | basename (set new-meta.title basename)) 23 | nil) 24 | 25 | (fn Meta [meta] 26 | "Set all manpage metadata not specified by --metadata foo=x" 27 | (each [k v (pairs new-meta)] 28 | (when (= nil (. meta k)) 29 | (d (: "Setting metadata '%s' to '%s' for manpage '%s'" :format k v 30 | PANDOC_STATE.output_file)) 31 | (tset meta k v))) 32 | meta) 33 | 34 | (fn Header [el] 35 | "Save first H1 for setting meta.header, replace with NAME 36 | NAME's contents are set to ' - " 37 | (if (= el.level 1) 38 | (when (not new-meta.header) 39 | (assert new-meta.title "meta.title not set") 40 | (set new-meta.header (pdutils.stringify el.content)) 41 | (d (: "Setting meta.header from first H1; adding NAME from meta.title: '%s - %s'" 42 | :format new-meta.title new-meta.header)) 43 | [(pd.Header 1 (pd.Str :NAME)) 44 | (pd.Para (pd.Inlines (pd.Str (.. new-meta.title " - " new-meta.header)))) 45 | (pd.Header 1 (pd.Str :DESCRIPTION))]) 46 | (doto el 47 | (tset :level (math.max 1 (- el.level 1)))))) 48 | 49 | (fn h1-upper [el] 50 | "Convert all level-1 headers to uppercase" 51 | (when (= 1 el.level) 52 | (pd.walk_block el {:Str (fn [el] (pd.Str (pdtext.upper el.text)))}))) 53 | 54 | (fn Table [el] 55 | "Format markdown tables into manpage-friendly format (not supported by pandoc)" 56 | (d (.. "formatting table for " PANDOC_STATE.output_file)) 57 | (let [rendered (pd.write (pd.Pandoc [el]) :plain) 58 | adjusted (-> rendered 59 | (: :gsub "%+([=:][=:]+)" 60 | #(.. " " (string.rep "-" (- (length $) 1)))) 61 | (: :gsub "(%+[-:][-:]+)" "") 62 | (: :gsub "%+\n" "\n") 63 | (: :gsub "\n| " "\n|") 64 | (: :gsub "|" ""))] 65 | [(pd.RawBlock :man ".RS -14n") 66 | (pd.CodeBlock adjusted) 67 | (pd.RawBlock :man :.RE)])) 68 | 69 | ;; TODO: process footnotes for output to manpage 70 | ;; by default, they don't appear to be emitted even without a blank Note 71 | ; (fn Note [el] {}) 72 | 73 | [{:Meta get-meta-header-and-title} 74 | {: Header} 75 | {:Header h1-upper} 76 | {: Table} 77 | {: Meta}] 78 | -------------------------------------------------------------------------------- /lua-primer.md: -------------------------------------------------------------------------------- 1 | # Lua Primer 2 | 3 | Once you've finished reading the tutorial, you may be wondering about 4 | the relationship between Fennel and Lua. If you have never programmed 5 | in Lua before, don't fear! It is one of the simplest programming 6 | languages ever. It's possible to learn Fennel without writing any Lua 7 | code, but for certain concepts there's no substitute for the Lua 8 | documentation. 9 | 10 | The book Programming in Lua is a great introduction. [The first 11 | edition][6] is available for free online and is still relevant, other 12 | than the section on modules. However, it's rather long, so if you have 13 | programmed before in other languages, you get going more quickly by 14 | focusing on specific areas where Lua is substantially different from 15 | other languages: 16 | 17 | Lua's types include: 18 | 19 | * [nil][7]: represents nothing, treated like false in conditionals 20 | * [booleans][8]: true and false 21 | * [numbers][9]: double-precision floating point only until integers added in 5.3 22 | * [strings][10]: immutable, may contain arbitrary binary data 23 | * [tables][11]: the only data structure 24 | * [coroutines][12]: a mechanism for pre-emptive multitasking 25 | * [userdata][13]: representing types that come from C code 26 | 27 | Of these, tables are by far the most complex as well as being the most 28 | different from what you may be used to in other languages. The most 29 | important consideration is that tables are used for both sequential 30 | data (aka lists, vectors, or arrays) as well as associative data (aka 31 | maps, dictionaries, or hashes). The same table can be used in both 32 | roles; whether a table is sequential or associative is not an inherent 33 | property of the table itself but determined by how a given piece of code 34 | interacts with the table. Iterating over a table with `ipairs` will 35 | treat it as an array, while `pairs` will treat it as an unordered 36 | key/value map. 37 | 38 | The [Lua reference manual][1] covers the entire language (including 39 | details of newer versions) in a more terse form which you may find 40 | more convenient when looking for specific things. The rest of this 41 | document provides a very brief overview of the standard library. 42 | 43 | Other Lua runtimes or embedded contexts usually introduce things that 44 | aren't covered here. 45 | 46 | ## Important top-level functions 47 | 48 | * `tonumber`: converts its string argument to a number; takes optional base 49 | * `tostring`: converts its argument to a string 50 | * `print`: prints `tostring` of all its arguments separated by tab characters 51 | * `type`: returns a string describing the type of its argument 52 | * `pcall`: calls a function in protected mode so errors are not fatal 53 | * `error`: halts execution and break to the nearest `pcall` 54 | * `assert`: raises an error if a condition is nil/false, otherwise returns it 55 | * `ipairs`: iterates over sequential tables 56 | * `pairs`: iterates over any table, sequential or not, in undefined order 57 | * `unpack`: turns a sequential table into multiple values (table.unpack in 5.2+) 58 | * `require`: loads and returns a given module 59 | 60 | Note that `tostring` on tables will give unsatisfactory results; simply 61 | evaluating the table in the REPL will invoke `fennel.view` for you, and 62 | show a human-readable view of the table (or you can invoke `fennel.view` 63 | explicitly in your code). 64 | 65 | ## The io module 66 | 67 | This module contains functions for operating on the filesystem. Note 68 | that directory listing is absent; you need the [luafilesystem][14] library 69 | for that. 70 | 71 | To open a file you use `io.open`, which returns a file descriptor upon 72 | success, or nil and a message upon failure. This failure behavior 73 | makes it well-suited for wrapping with `assert` to turn failure into 74 | an error. You can call methods on the file descriptor, concluding with 75 | `f:close`. 76 | 77 | ```fennel 78 | (let [f (assert (io.open "path/to/file"))] 79 | (print (f:read)) ; reads a single line by default 80 | (print (f:read "*a")) ; you can read the whole file 81 | (f:close)) 82 | ``` 83 | 84 | You can also call `io.open` with `:w` as its second argument to open 85 | the file in write mode and then call `f:write` and `f:flush` on the 86 | file descriptor. 87 | 88 | The other important function in this module is the `io.lines` 89 | function, which returns an iterator over all the file's lines. 90 | 91 | ```fennel 92 | (each [line (io.lines "path/to/file")] 93 | (process-line line)) 94 | ``` 95 | 96 | It will automatically close the file once it detects the end of the 97 | file. You can also call `f:lines` on a file descriptor that you got 98 | using `io.open`. 99 | 100 | ## The table module 101 | 102 | This contains some basic table manipulation functions. All these 103 | functions operate on sequential tables, not general key/value tables. 104 | The most important ones are described below: 105 | 106 | The `table.insert` function takes a table, an optional position, and 107 | an element, and it will insert the element into the table at that 108 | position. The position defaults to being the end of the 109 | table. Similarly `table.remove` takes a table and an optional 110 | position, removes the element at that position, and returns it. The 111 | position defaults to the last element in the table. To remove 112 | something from a non-sequential table, simply set its key to nil. 113 | 114 | The `table.concat` function returns a string that has all the elements 115 | concatenated together with an optional separator. 116 | 117 | ```fennel 118 | (let [t [1 2 3]] 119 | (table.insert t 2 "a") ; t is now [1 "a" 2 3] 120 | (table.insert t "last") ; now [1 "a" 2 3 "last"] 121 | (print (table.remove t)) ; prints "last" 122 | (table.remove t 1) ; t is now ["a" 2 3] 123 | (print (table.concat t ", "))) prints "a, 2, 3" 124 | ``` 125 | 126 | The `table.sort` function sorts a table in-place, as a side-effect. It 127 | takes an optional comparator function which should return true when 128 | its first argument is less than the second. 129 | 130 | The `table.unpack` function returns all the elements in the table as 131 | multiple values. Note that `table.unpack` is just `unpack` in Lua 5.1. 132 | 133 | It's not part of the `table` module, but the `next` function works 134 | with tables. It's most commonly used to detect if a table is empty, 135 | since calling it with a single table argument will return nil for 136 | empty tables. But it can also be used to step thru a table without 137 | iterators, for example: 138 | 139 | ```fennel 140 | (fn find [t x ?k] 141 | (match (next t ?k) 142 | (k x) k 143 | (k y) (find t x k))) 144 | ``` 145 | 146 | ## Other important modules 147 | 148 | You can explore a module by evaluating it in the REPL to display all 149 | the functions and values it contains. 150 | 151 | * `math`: all your standard math functions, trig, pseudorandom generator, etc 152 | * `string`: common string operations 153 | * `os`: operating system functions like `exit`, `time`, `getenv`, etc 154 | 155 | ## What's missing 156 | 157 | Most programming languages have a much larger standard library than 158 | Lua. You may be surprised to find that things you take for granted 159 | require third-party libraries in Lua. 160 | 161 | Lua does not implement regular expressions but its own more limited 162 | [pattern][2] language for `string.find`, `string.match`, etc. 163 | 164 | The lack of a `string.split` function surprises many people. However, 165 | the `string.gmatch` function used with `icollect` can serve to split 166 | strings into a table. Or if you just need an iterator to loop over, 167 | you can use `string.gmatch` directly and skip `icollect`. 168 | 169 | ```fennel 170 | (let [str "hello there, world"] 171 | (icollect [s (string.gmatch str "[^ ]+")] s)) 172 | ;; -> ["hello" "there," "world"] 173 | ``` 174 | 175 | You can launch subprocesses with [io.popen][15] but note that you can 176 | only write to its input or read from its output; [doing both][16] 177 | cannot be done safely without some form of concurrency. 178 | 179 | Networking requires a 3rd-party library like [luasocket][17]. 180 | 181 | ## Advanced 182 | 183 | * `_G`: a table of all globals 184 | * `getfenv`/`setfenv`: access to first-class function environments in 185 | Lua 5.1; in 5.2 onward use [the _ENV table][3] instead 186 | * `getmetatable`/`setmetatable`: metatables allow you to 187 | [override the behavior of tables][4] 188 | in flexible ways with functions of your choice 189 | * `coroutine`: the coroutine module allows you to do 190 | [flexible control transfer][5] in a first-class way 191 | * `package`: this module tracks and controls the loading of modules 192 | * `arg`: table of command-line arguments passed to the process 193 | * `...`: arguments passed to the current function; acts as multiple values 194 | * `select`: most commonly used with `...` to find the number of arguments 195 | * `xpcall`: acts like `pcall` but accepts a handler; used to get a 196 | full stack trace rather than a single line number for errors 197 | 198 | The `...` values also work at the top level of a file. They are 199 | usually used to capture command-line arguments for files run directly 200 | from the command line, but they can also pass on values from a 201 | `dofile` call or tell you the name of the current module in a file 202 | that's loaded from `require`. Note that since `...` represents 203 | multiple values it is common to put it in a table to store it, unless 204 | the number of values is known ahead of time: 205 | 206 | ```fennel 207 | (local (first-arg second-arg) ...) 208 | (local all-args [...]) 209 | ``` 210 | 211 | ## Lua loading 212 | 213 | These are used for loading Lua code. The `load*` functions return a 214 | "chunk" function which must be called before the code gets run, but 215 | `dofile` executes immediately. 216 | 217 | * `dofile` 218 | * `load` 219 | * `loadfile` 220 | * `loadstring` 221 | 222 | ## Obscure 223 | 224 | * `_VERSION`: the current version of Lua being used as a string 225 | * `collectgarbage`: you hopefully will never need this 226 | * `debug`: see the Lua manual for this module 227 | * `rawequal`/`rawget`/`rawlen`/`rawset`: operations which bypass metatables 228 | 229 | [1]: https://www.lua.org/manual/5.1/ 230 | [2]: https://www.lua.org/pil/20.2.html 231 | [3]: http://leafo.net/guides/setfenv-in-lua52-and-above.html 232 | [4]: https://www.lua.org/pil/13.html 233 | [5]: http://leafo.net/posts/itchio-and-coroutines.html 234 | [6]: https://www.lua.org/pil/contents.html 235 | [7]: https://www.lua.org/pil/2.1.html 236 | [8]: https://www.lua.org/pil/2.2.html 237 | [9]: https://www.lua.org/pil/2.3.html 238 | [10]: https://www.lua.org/pil/2.4.html 239 | [11]: https://www.lua.org/pil/11.html 240 | [12]: https://www.lua.org/pil/9.1.html 241 | [13]: https://www.lua.org/pil/28.html 242 | [14]: https://lunarmodules.github.io/luafilesystem/ 243 | [15]: https://www.lua.org/manual/5.4/manual.html#pdf-io.popen 244 | [16]: http://lua-users.org/lists/lua-l/2007-10/msg00189.html 245 | [17]: https://lunarmodules.github.io/luasocket/ 246 | -------------------------------------------------------------------------------- /man/man1/fennel.1: -------------------------------------------------------------------------------- 1 | .TH FENNEL 1 2 | 3 | .SH NAME 4 | fennel \- a lisp programming language that runs on Lua 5 | .SH SYNOPSIS 6 | .B fennel 7 | [\fB--repl\fR] | 8 | [\fB--compile \fIfilename\fR] | 9 | [\fB--eval \fIsource\fR] | 10 | [\fIfilename\fP] [\fIargs ...\fR] 11 | 12 | .SH DESCRIPTION 13 | This manual page documents briefly the 14 | .B fennel 15 | command. 16 | .PP 17 | .B fennel 18 | is the main entry point for Fennel, a lisp programming language that 19 | runs on Lua runtimes. With no options or arguments, it runs an 20 | interactive Read-Eval-Print loop (REPL). 21 | .PP 22 | Given a filename as its first argument, it runs that file and passes 23 | it the subsequent arguments. Ahead-of-time compilation can be invoked 24 | with the 25 | .B --compile 26 | flag, while short snippets can be evaluated with the 27 | .B --eval 28 | argument. 29 | 30 | .SH OPTIONS 31 | A summary of options is included below. 32 | .TP 33 | .B \-\-repl 34 | Start an interactive repl session. This is the default when given no arguments. 35 | .TP 36 | .B \-\-compile \fIfilename\fP 37 | Perform ahead-of-time compilation on the provided file and write the 38 | Lua output to standard out. 39 | .TP 40 | .B \-\-eval \fIsource\fP 41 | Evaluate a given piece of source code and print the result. 42 | .TP 43 | .B \-\-no-searcher 44 | When running a repl or a file, 45 | .B fennel.searcher 46 | is installed by default so that the 47 | .B require 48 | function can load Fennel files in addition to Lua files. This flag 49 | disables that behavior. Has no effect for ahead-of-time compilation. 50 | .TP 51 | .B \-\-add-package-path \fIpath\fP 52 | Add the given path to 53 | .B package.path 54 | so that the 55 | .B require 56 | function will know to look there when searching for Lua modules. 57 | .TP 58 | .B \-\-add-fennel-path \fIpath\fP 59 | Same as above, but for Fennel's path used when searching for Fennel 60 | modules. 61 | .TP 62 | .B \-\-globals \fIVAR1[,VAR2...]\fP 63 | Allow VAR1, VAR2, etc as globals in addition to the standard set of 64 | globals. This enables strict global checking even in ahead-of-time 65 | compilation where it otherwise would be disabled. Use "*" to disable 66 | globals checking. 67 | .TP 68 | .B \-\-globals-only \fIVAR1[,VAR2...]\fP 69 | Same as above, but without the inclusion of the standard set of globals. 70 | .TP 71 | .B \-\-require-as-include 72 | Instead of loading required modules at runtime, compile them inline 73 | into the main file being compiled. Only useful during ahead-of-time 74 | compilation. 75 | .TP 76 | .B \-\-assert-as-repl 77 | Calls to the built-in function 78 | .B assert 79 | from Fennel will be replaced with calls to 80 | .B assert-repl 81 | so that when the assertion fails, a REPL will be started in which you 82 | can interactively debug. 83 | .TP 84 | .B \-\-use-bit-lib 85 | Compile bitwise operations to use LuaJIT's bitop library instead of Lua 86 | 5.3+ bitwise operators. 87 | .TP 88 | .B \-\-load \fIFILE\fP 89 | Load the specified file before any command is run. 90 | .TP 91 | .B \-\-compile-binary \fIFILE\fP \fIOUT\fP \fILUA_LIB\fP \fILUA_DIR\fP 92 | Compile FILE to a standalone binary OUT using LUA_LIB and the Lua 93 | header files in LUA_DIR. See 94 | .B \-\-compile-binary \-\-help 95 | for details. 96 | .TP 97 | .B \-\-no\-compiler\-sandbox 98 | Do not limit compiler environment (used in macros) to minimal sandbox. 99 | .TP 100 | .B \-\-keywords \fIKEYWORD1[,KEYWORD2...]\fP 101 | Treat these symbols as reserved Lua keywords. 102 | .TP 103 | .B \-h, \-\-help 104 | Print a help message and exit 105 | .TP 106 | .B \-v, \-\-version 107 | Print the version number and exit 108 | .PP 109 | Use the NO_COLOR environment variable to disable escape codes in error messages. 110 | 111 | .SH SEE ALSO 112 | 113 | .MR fennel-api 3 , 114 | .MR fennel-reference 5 , 115 | .MR fennel-tutorial 7 116 | 117 | The semantics are very close to Lua, so Lua's reference manual is also helpful. 118 | 119 | .SH COMMUNITY 120 | 121 | The mailing list is at https://lists.sr.ht/~technomancy/fennel while 122 | the issue tracker is at https://todo.sr.ht/~technomancy/fennel. Most 123 | discussion happens on the #fennel channel of Libera chat. 124 | 125 | .SH AUTHORS 126 | Calvin Rose and Phil Hagelberg and contributors: 127 | https://github.com/bakpakin/Fennel/graphs/contributors 128 | 129 | .SH LICENSE 130 | Copyright © 2016-2024, Released under the MIT/X11 license 131 | -------------------------------------------------------------------------------- /rationale.md: -------------------------------------------------------------------------------- 1 | # Why Fennel? 2 | 3 | Fennel is a programming language that runs on the Lua runtime. 4 | 5 | ## Why Lua? 6 | 7 | The Lua programming language is an excellent and very underrated tool. It is 8 | remarkably powerful yet keeps a very small footprint both conceptually as a 9 | language and in terms of the size of its implementation. (The reference 10 | implementation consists of about nineteen thousand lines of C and compiles to 11 | 278kb.) Partly because it is so simple, Lua is also extremely fast. But the 12 | most important thing about Lua is that it's specifically designed to be put 13 | in other programs to make them reprogrammable by the end user. 14 | 15 | The conceptual simplicity of Lua stands in stark contrast to other "easy to 16 | learn" languages like JavaScript or Python--Lua contains very close to the 17 | minimum number of ideas needed to get the job done; only Forth and Scheme 18 | offer a comparable simplicity. When you combine this meticulous simplicity 19 | with the emphasis on making programs reprogrammable, the result is a powerful 20 | antidote to prevailing trends in technology of treating programs as black 21 | boxes out of the control of the user. 22 | 23 | ## And yet... 24 | 25 | So if Lua is so great, why not just use Lua? In many cases you should! But 26 | there are a handful of shortcomings in Lua which over time have shown to be 27 | error-prone or unclear. Fennel runs on Lua, and the runtime semantics of 28 | Fennel are a subset of Lua's, but you can think of Fennel as an alternate 29 | notation you can use to write Lua programs which helps you avoid common 30 | pitfalls. This allows Fennel to focus on doing one thing very well and not get 31 | dragged down with things like implementing a virtual machine, a standard 32 | library, or profilers and debuggers. Any library or tool that already works 33 | for Lua will work just as well for Fennel. 34 | 35 | The most obvious difference between Lua and Fennel is the parens-first 36 | syntax; Fennel belongs to the Lisp family of programming languages. You could 37 | say that this removes complexity from the grammar; the paren-based syntax is 38 | more regular and has fewer edge cases. Simply by virtue of being a lisp, 39 | Fennel removes from Lua: 40 | 41 | * statements (everything is an expression), 42 | * operator precedence (there is no ambiguity about what comes first), and 43 | * early returns (functions always return in tail positions). 44 | 45 | ## Variables 46 | 47 | One of the most common legitimate criticisms leveled at Lua is that it makes 48 | it easy to accidentally use globals, either by forgetting to add a `local` 49 | declaration or by making a typo. Fennel allows you to use globals in the rare 50 | case they are necessary but makes it very difficult to use them by accident. 51 | 52 | Fennel also removes the ability to reassign normal locals. If you declare a 53 | variable that will be reassigned, you must introduce it with `var` 54 | instead. This encourages cleaner code and makes it obvious at a glance when 55 | reassignment is going to happen. Note that Lua 5.4 introduced a similar idea 56 | with `` variables, but since Fennel did not have to keep decades of 57 | existing code like Lua it was able to make the cleaner choice be the default 58 | rather than opt-in. 59 | 60 | ## Tables and Loops 61 | 62 | Lua's notation for tables (its data structure) feels somewhat dated. It uses 63 | curly brackets for both sequential (array-like) and key/value 64 | (dictionary-like) tables, while Fennel uses the much more familiar notation 65 | of using square brackets for sequential tables and curly brackets for 66 | key/value tables. 67 | 68 | In addition Lua overloads the `for` keyword for both numeric "count from X to 69 | Y" style loops as well as more generic iterator-based loops. Fennel 70 | uses `for` in the first case and introduces the `each` form for the latter. 71 | 72 | ## Functions 73 | 74 | Another common criticism of Lua is that it lacks arity checks; that is, if 75 | you call a function without enough arguments, it will simply proceed instead 76 | of indicating an error. Fennel allows you to write functions that work this 77 | way (`fn`) when it's needed for speed, but it also lets you write functions 78 | which check for the arguments they expect using `lambda`. 79 | 80 | ## Other 81 | 82 | If you've been programming in newer languages, you are likely to be spoiled 83 | by pervasive destructuring of data structures when binding variables, as well 84 | as by pattern matching to write more declarative conditionals. Both these are 85 | absent from Lua and included in Fennel. 86 | 87 | Finally Fennel includes a macro system so that you can easily extend the 88 | language to include new syntactic forms. This feature is intentionally listed 89 | last because while lisp programmers have historically made a big deal about 90 | how powerful it is, it is relatively rare to encounter situations where such 91 | a powerful construct is justified. 92 | 93 | For a more detailed look at the guiding principles of Fennel from a 94 | design perspective see [the Values of Fennel](https://fennel-lang.org/values). 95 | -------------------------------------------------------------------------------- /security.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | Please report potential security issues with the Fennel compiler or 4 | web site to [phil@hagelb.org][1] and [jaawerth@gmail.com][2]. 5 | 6 | Sensitive reports may be encrypted with the PGP key listed below. 7 | 8 | ## Signatures 9 | 10 | From version 0.10.0 onward, Fennel releases and tags have been signed 11 | with the PGP key [8F2C85FFC1EBC016A3B683DE8BD38C28CCFD2DA6][3]. 12 | Before that the key [20242BACBBE95ADA22D0AFD7808A33D379C806C3][4] was used. 13 | 14 | To verify: 15 | 16 | $ curl https://technomancy.us/8F2C85FFC1EBC016A3B683DE8BD38C28CCFD2DA6.txt | gpg --import - 17 | $ gpg --verify fennel-1.2.0.asc 18 | 19 | From 1.0 onwards, releases are also signed with `.sig` files using SSH keys: 20 | 21 | $ curl -o allowed https://fennel-lang.org/downloads/allowed_signers 22 | $ ssh-keygen -Y verify -f allowed -I phil@hagelb.org -n file -s fennel-1.2.0.sig < fennel-1.2.0 23 | 24 | You can compare the key in the [allowed][5] file with the keys 25 | published at [technomancy.us][6], [SourceHut][7], or [GitHub][8]. 26 | 27 | ## Historical Issues 28 | 29 | In versions from 1.0.0 to 1.3.1, it was possible for code running in 30 | the compiler sandbox to call un-sandboxed functions from applications 31 | or Fennel libraries when running with metadata enabled. This could 32 | result in RCE when evaluating untrusted code in a way that relied on 33 | the sandbox for services running with metadata enabled. 34 | 35 | In addition, even when metadata was disabled, it was still possible 36 | for sandboxed code to trigger loading of a module already on the load 37 | path. In most cases if an attacker can get a file on the load-path 38 | then they've already won, but in the context of tools that run static 39 | analysis on untrusted code, this could result in a vulnerability. 40 | 41 | Versions prior to 1.0.0 did not sandbox macros. 42 | 43 | [1]: mailto:phil@hagelb.org 44 | [2]: mailto:jaawerth@gmail.com 45 | [3]: https://technomancy.us/8F2C85FFC1EBC016A3B683DE8BD38C28CCFD2DA6.txt 46 | [4]: https://technomancy.us/20242BACBBE95ADA22D0AFD7808A33D379C806C3.txt 47 | [5]: https://fennel-lang.org/downloads/allowed_signers 48 | [6]: https://technomancy.us/keys 49 | [7]: https://meta.sr.ht/~technomancy.keys 50 | [8]: https://github.com/technomancy.keys 51 | -------------------------------------------------------------------------------- /setup.md: -------------------------------------------------------------------------------- 1 | # Setting up Fennel 2 | 3 | This document will guide you through setting up Fennel on your 4 | computer. This document assumes you know how to run shell commands and 5 | edit configuration files in a UNIX-like environment. 6 | 7 | **Note**: Fennel can be used in non-UNIX environments, but those environments 8 | will mostly not be covered in this document. 9 | 10 | Fennel does not contain any telemetry/spyware and never will. 11 | 12 | 13 | ## Downloading Fennel 14 | 15 | Downloading Fennel on your computer allows you to run Fennel code and 16 | compile to Lua. You have a few options for how to install Fennel. 17 | 18 | 19 | ### Downloading Fennel with a package manager 20 | 21 | Depending on what package manager you use on your system, you may be 22 | able to use it to install Fennel. See [the 23 | wiki](https://wiki.fennel-lang.org/Packaging) for a list of packaging 24 | systems which offer Fennel. Packaged versions of Fennel may lag behind 25 | the official releases and may only support one version at a time, 26 | but they tend to be the most convenient and support automatic updates. 27 | For instance, if you use Fedora, it should be as easy as running 28 | `sudo dnf install fennel`. 29 | 30 | 31 | ### Downloading the fennel script 32 | 33 | This method assumes you have Lua 5.1, 5.2, 5.3, 5.4, or LuaJIT 34 | installed on your system. 35 | 36 | This method requires you to manually update the `fennel` script when 37 | you want to use a newer version that has come out. 38 | 39 | 1. Download [the fennel script](https://fennel-lang.org/downloads/fennel-1.5.3) 40 | 2. Run `chmod +x fennel-1.5.3` to make it executable 41 | 3. Download [and verify](https://fennel-lang.org/security#signatures) 42 | the [signature](https://fennel-lang.org/downloads/fennel-1.5.3.asc) 43 | (optional). 44 | 4. Move `fennel-1.5.3` to a directory on your `$PATH`, such as `/usr/local/bin` 45 | 46 | **Note**: You can rename the script to `fennel` for convenience. Or 47 | you can leave the version in the name, which makes it easy to keep 48 | many versions of Fennel installed at once. 49 | 50 | 51 | ### Downloading a Fennel binary 52 | 53 | Downloading a Fennel binary allows you to run Fennel on your computer without 54 | having to download Lua, if you are on a supported platform. If you 55 | already have Lua installed, it's better to use the script above. 56 | 57 | This method requires you to manually update the `fennel` binary when 58 | you want to use a newer version that has come out. 59 | 60 | 1. Choose one the options below, depending on your system: 61 | - [GNU/Linux x86_64](https://fennel-lang.org/downloads/fennel-1.5.3-x86_64) 62 | ([signature](https://fennel-lang.org/downloads/fennel-1.5.3-x86_64.asc)) 63 | - [Windows](https://fennel-lang.org/downloads/fennel-1.5.3.exe) 64 | ([signature](https://fennel-lang.org/downloads/fennel-1.5.3.exe.asc)) 65 | 2. Run `chmod +x fennel-1.5.3*` to make it executable 66 | 3. Download [and verify](https://fennel-lang.org/security#signatures) the signature 67 | (optional). 68 | 4. Move the downloaded binary to a directory on your `$PATH`, such as `/usr/local/bin` 69 | 70 | 71 | ## Embedding Fennel 72 | 73 | Fennel code can be embedded inside of applications that support Lua 74 | either by including the Fennel compiler inside of the application, 75 | or by performing ahead-of-time compilation. Embedding Fennel in a 76 | program that doesn't already support Lua is possible but outside the 77 | scope of this document. 78 | 79 | **Note**: Embedding the Fennel compiler in an application is the more 80 | flexible option, and is recommended. By embedding the Fennel compiler 81 | in an application, users can write their own extension scripts in 82 | Fennel to interact with the application, and you can reload during 83 | development. If the application is more restricted, (for instance, if 84 | you can only embed one Lua file into the application and it cannot 85 | access the disk to load further files) then compiling Fennel code to 86 | Lua during the build process and including the Lua output in the 87 | application may be easier. 88 | 89 | There are so many ways to distribute your code that we can't cover 90 | them all here; please [see the wiki page on distribution for details](https://wiki.fennel-lang.org/Distribution). 91 | 92 | 93 | ### Embedding the Fennel compiler in a Lua application 94 | 95 | The Fennel compiler can be added to your application and then loaded from Lua. 96 | 97 | 1. Add [fennel.lua](https://fennel-lang.org/downloads/fennel-1.5.3.lua) to your code repository. 98 | 2. Add the following line to your Lua code: 99 | 100 | ```lua 101 | require("fennel").install().dofile("main.fnl") 102 | ``` 103 | 104 | Replace `main.fnl` with whatever filename you use as an entry 105 | point. You can pass [options](api.md) to the fennel compiler by 106 | passing a table to the `install` function. 107 | 108 | Be sure to use the `fennel.lua` library and not the file for the 109 | entire `fennel` executable. 110 | 111 | ### Performing ahead-of-time compilation 112 | 113 | If the target system of your application does not make it easy to add 114 | the Fennel compiler but has Lua installed, Fennel offers ahead-of-time 115 | (AOT) compilation. This allows you to compile `.fnl` files to `.lua` 116 | files before shipping an application. 117 | 118 | This section will guide you through updating a `Makefile` to perform 119 | this compilation for you; if you use a different build system you can 120 | adapt it. 121 | 122 | 1. Add the following lines to your `Makefile`: 123 | 124 | ``` 125 | %.lua: %.fnl fennel 126 | ./fennel --compile $< > $@ 127 | ``` 128 | 129 | 2. Ensure your build target depends on the `.lua` files you need, for 130 | example, if every `.fnl` file has a corresponding `.lua` file: 131 | 132 | ``` 133 | SRC := $(wildcard *.fnl) 134 | OUT := $(patsubst %.fnl,%.lua,$(SRC)) 135 | myprogram: $(OUT) 136 | [...] 137 | ``` 138 | 139 | 140 | **Note 1**: Ahead-of-time compilation is also useful if what you are 141 | working with requires optimal startup time. "Fennel compiles fast, 142 | but not as fast as not having to compile." -- jaawerth 143 | 144 | **Note 2**: It's recommended you include the `fennel` script in your 145 | repository to get consistent results rather than relying on an 146 | arbitrary version of Fennel that is installed on your machine at the 147 | time of building. 148 | 149 | 150 | ## Adding Fennel support to your text editor 151 | 152 | You can write Fennel code in any editor, but some editors make it more 153 | comfortable than others. Most people find support for syntax 154 | highlighting, automatic indentation, and delimiter matching 155 | convenient, as working without these features can feel tedious. 156 | 157 | Other editors support advanced features like an integrated REPL, live 158 | reloading while you edit the program, documentation lookups, and 159 | jumping to source definitions. 160 | 161 | See [the wiki](https://wiki.fennel-lang.org/Editors) 162 | for a list of editors that have Fennel support. 163 | 164 | If your editor supports the Language Server Protocol (LSP) then you 165 | can install [fennel-ls](https://git.sr.ht/~xerool/fennel-ls) to get 166 | highlighting of errors and improved navigation. 167 | 168 | 169 | ## Adding readline support to Fennel 170 | 171 | The command-line REPL that comes with the `fennel` script works out of the box, but 172 | the built-in line-reader is very limited in user experience. Adding 173 | [GNU Readline](https://tiswww.case.edu/php/chet/readline/rltop.html) 174 | support enables user-friendly features, such as: 175 | 176 | - tab-completion on the REPL that can complete on all locals, macros, and special forms 177 | - a rolling history buffer, which can be navigated, searched (`ctrl+r`), and optionally 178 | persisted to disk so you can search input from previous REPL sessions 179 | - Emacs (default) or vi key binding emulation via readline's custom support for better line 180 | navigation 181 | - optional use of additional readline features in `~/.inputrc`, such as blinking 182 | on matched parentheses or color output (described below) 183 | 184 | See [the wiki page on readline](https://wiki.fennel-lang.org/Readline) 185 | for details of how to install and configure it on your system. 186 | 187 | ## Making games with Fennel 188 | 189 | The two main platforms for making games with Fennel are 190 | [TIC-80](https://tic80.com) and [LÖVE](https://love2d.org/). 191 | 192 | TIC-80 is software that acts as a simulated computer in which you can write 193 | code, design art, compose music, and lay out maps for games. TIC-80 194 | also makes it easy for you to publish and share the games you make 195 | with others. TIC-80 introduces restrictions such as low resolution and 196 | limited memory to emulate retro game styles. 197 | 198 | LÖVE is a game-making framework for the Lua programming language. LÖVE 199 | is more flexible than TIC-80 in that it allows you to import from 200 | external resources and use any resolution or memory you like, but at a 201 | cost in that it is more complicated to make games in and more 202 | difficult to run in the browser. 203 | 204 | Both TIC-80 and LÖVE offer cross-platform support across Windows, Mac, 205 | and Linux systems, but TIC-80 games can be played in the browser and 206 | LÖVE games cannot without more complex 3rd-party tools. 207 | 208 | The [Fennel wiki](https://wiki.fennel-lang.org/Codebases) links 209 | to many games made in both systems you can study. 210 | 211 | 212 | ### Using Fennel in TIC-80 213 | 214 | Support for Fennel is built into TIC-80. If you want to use the 215 | built-in text editor, you don't need any other tools, just launch 216 | TIC-80 and run `new fennel` in its console to get started. 217 | 218 | The [TIC-80 wiki](https://github.com/nesbox/TIC-80/wiki) documents 219 | the functions to use and important concepts. 220 | 221 | All TIC-80 games allow you to view and edit the source and assets. Try 222 | loading this [Conway's Life](https://tic80.com/play?cart=656) game 223 | to see how it's made: 224 | 225 | * Click "start" to begin 226 | * Press the Esc key and click "Close game" 227 | * Press Esc again to see the code 228 | 229 | 230 | ### Using Fennel with LÖVE 231 | 232 | LÖVE has no built-in support for Fennel, so you will need to setup 233 | support yourself, similar to [Embedding Fennel](#embedding-fennel) above. 234 | 235 | This [project skeleton for LÖVE](https://gitlab.com/alexjgriffith/min-love2d-fennel) 236 | shows you how to setup support for Fennel and how to setup a 237 | console-based REPL for debugging your game while it runs. 238 | 239 | You can reference the [LÖVE wiki](https://love2d.org/wiki/Main_Page) 240 | for Lua-specific documentation. Use [See Fennel](/see) to see how any 241 | given Lua snippet would look translated to Fennel. 242 | -------------------------------------------------------------------------------- /src/fennel.fnl: -------------------------------------------------------------------------------- 1 | ;; Copyright © Calvin Rose and contributors 2 | ;; Permission is hereby granted, free of charge, to any person obtaining a copy 3 | ;; of this software and associated documentation files (the "Software"), to 4 | ;; deal in the Software without restriction, including without limitation the 5 | ;; rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | ;; sell copies of the Software, and to permit persons to whom the Software is 7 | ;; furnished to do so, subject to the following conditions: The above copyright 8 | ;; notice and this permission notice shall be included in all copies or 9 | ;; substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", 10 | ;; WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 11 | ;; TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 12 | ;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 13 | ;; LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 14 | ;; CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | ;; SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | ;; This module ties everything else together; it's the public interface of 18 | ;; the compiler. All other modules should be considered implementation details 19 | ;; subject to change. 20 | 21 | (local utils (require :fennel.utils)) 22 | (local parser (require :fennel.parser)) 23 | (local compiler (require :fennel.compiler)) 24 | (local specials (require :fennel.specials)) 25 | (local repl (require :fennel.repl)) 26 | (local view (require :fennel.view)) 27 | 28 | (fn eval-env [env opts] 29 | (if (= env :_COMPILER) 30 | (let [env (specials.make-compiler-env nil compiler.scopes.compiler {} opts)] 31 | (collect [k v (pairs (or opts.extra-env {})) &into env] k v) 32 | ;; re-enable globals-checking; previous globals-checking below doesn't 33 | ;; work on the compiler env because of the sandbox. 34 | (when (= opts.allowedGlobals nil) 35 | (set opts.allowedGlobals (specials.current-global-names env))) 36 | (specials.wrap-env env)) 37 | (and env (specials.wrap-env env)))) 38 | 39 | (fn eval-opts [options str] 40 | (let [opts (utils.copy options)] 41 | ;; eval and dofile are considered "live" entry points, so we can assume 42 | ;; that the globals available at compile time are a reasonable allowed list 43 | (when (= opts.allowedGlobals nil) 44 | (set opts.allowedGlobals (specials.current-global-names opts.env))) 45 | ;; if the code doesn't have a filename attached, save the source in order 46 | ;; to provide targeted error messages. 47 | (when (and (not opts.filename) (not opts.source)) 48 | (set opts.source str)) 49 | (when (= opts.env :_COMPILER) 50 | (set opts.scope (compiler.make-scope compiler.scopes.compiler))) 51 | opts)) 52 | 53 | (fn eval [str ?options ...] 54 | (let [opts (eval-opts ?options str) 55 | env (eval-env opts.env opts) 56 | lua-source (compiler.compile-string str opts) 57 | loader (specials.load-code lua-source env 58 | (if opts.filename 59 | (.. "@" opts.filename) 60 | str))] 61 | (set opts.filename nil) 62 | (loader ...))) 63 | 64 | (fn dofile* [filename ?options ...] 65 | (let [opts (utils.copy ?options) 66 | f (assert (io.open filename :rb)) 67 | source (assert (f:read :*all) (.. "Could not read " filename))] 68 | (f:close) 69 | (set opts.filename filename) 70 | (eval source opts ...))) 71 | 72 | (fn syntax [] 73 | "Return a table describing the callable forms known by Fennel." 74 | (let [body? [:when :with-open :collect :icollect :fcollect :lambda :λ 75 | :macro :match :match-try :case :case-try :accumulate 76 | :faccumulate :doto] 77 | binding? [:collect :icollect :fcollect :each :for :let :with-open 78 | :accumulate :faccumulate] 79 | define? [:fn :lambda :λ :var :local :macro :macros :global] 80 | deprecated ["~=" "#" :global :require-macros :pick-args] 81 | out {}] 82 | (each [k v (pairs compiler.scopes.global.specials)] 83 | (let [metadata (or (. compiler.metadata v) {})] 84 | (tset out k {:special? true :body-form? metadata.fnl/body-form? 85 | :binding-form? (utils.member? k binding?) 86 | :define? (utils.member? k define?) 87 | :deprecated? (utils.member? k deprecated)}))) 88 | (each [k (pairs compiler.scopes.global.macros)] 89 | (tset out k {:macro? true :body-form? (utils.member? k body?) 90 | :binding-form? (utils.member? k binding?) 91 | :define? (utils.member? k define?)})) 92 | (each [k v (pairs _G)] 93 | (case (type v) 94 | :function (tset out k {:global? true :function? true}) 95 | :table (when (not (k:find "^_")) 96 | (each [k2 v2 (pairs v)] 97 | (when (= :function (type v2)) 98 | (tset out (.. k "." k2) {:function? true :global? true}))) 99 | (tset out k {:global? true})))) 100 | out)) 101 | 102 | ;; The public API module we export: 103 | (local mod {;; AST functions 104 | :list utils.list 105 | :list? utils.list? 106 | :sym utils.sym 107 | :sym? utils.sym? 108 | :multi-sym? utils.multi-sym? 109 | :sequence utils.sequence 110 | :sequence? utils.sequence? 111 | :table? utils.table? 112 | :comment utils.comment 113 | :comment? utils.comment? 114 | :varg utils.varg 115 | :varg? utils.varg? 116 | ;; parsing 117 | :sym-char? parser.sym-char? 118 | :parser parser.parser 119 | ;; compiling 120 | :compile compiler.compile 121 | :compile-string compiler.compile-string 122 | ;; running code 123 | : eval 124 | : repl 125 | : view 126 | :dofile dofile* 127 | :load-code specials.load-code 128 | ;; examining 129 | :doc specials.doc 130 | :metadata compiler.metadata 131 | :traceback compiler.traceback 132 | :getinfo compiler.getinfo 133 | :version utils.version 134 | :runtime-version utils.runtime-version 135 | :ast-source utils.ast-source 136 | ;; finding code 137 | :path utils.path 138 | :macro-path utils.macro-path 139 | :macro-loaded specials.macro-loaded 140 | :macro-searchers specials.macro-searchers 141 | :search-module specials.search-module 142 | :make-searcher specials.make-searcher 143 | :searcher (specials.make-searcher) 144 | : syntax 145 | ;; deprecated; you probably don't want these 146 | :gensym compiler.gensym 147 | :scope compiler.make-scope 148 | :mangle compiler.global-mangling 149 | :unmangle compiler.global-unmangling 150 | :compile1 compiler.compile1 151 | :string-stream parser.string-stream 152 | :granulate parser.granulate 153 | ;; backwards-compatibility aliases 154 | :loadCode specials.load-code 155 | :make_searcher specials.make-searcher 156 | :makeSearcher specials.make-searcher 157 | :searchModule specials.search-module 158 | :macroPath utils.macro-path 159 | :macroSearchers specials.macro-searchers 160 | :macroLoaded specials.macro-loaded 161 | :compileStream compiler.compile-stream 162 | :compileString compiler.compile-string 163 | :compile-stream compiler.compile-stream 164 | :stringStream parser.string-stream 165 | :runtimeVersion utils.runtime-version}) 166 | 167 | (fn mod.install [?opts] 168 | (table.insert (or package.searchers package.loaders) 169 | (specials.make-searcher ?opts)) 170 | mod) 171 | 172 | ;; This is bad; we have a circular dependency between the specials section and 173 | ;; the evaluation section due to require-macros/import-macros, etc. For now 174 | ;; stash it in the utils table, but we should untangle it 175 | (set utils.fennel-module mod) 176 | 177 | (macro embed-src [filename] 178 | `(eval-compiler (when _G.io (with-open [f# (assert (_G.io.open ,filename))] 179 | (.. "[===[" (f#:read :*all) "]===]"))))) 180 | 181 | (fn load-macros [src env] 182 | (let [chunk (assert (specials.load-code src env))] 183 | (each [k v (pairs (chunk utils specials.get-function-metadata))] 184 | (tset compiler.scopes.global.macros k v)))) 185 | 186 | ;; Load the built-in macros from macros.fnl and match.fnl 187 | (let [env (specials.make-compiler-env nil compiler.scopes.compiler {})] 188 | (load-macros (embed-src "bootstrap/macros.lua") env) 189 | (load-macros (embed-src "bootstrap/match.lua") env)) 190 | 191 | mod 192 | -------------------------------------------------------------------------------- /src/fennel/binary.fnl: -------------------------------------------------------------------------------- 1 | ;; This module compiles Fennel modules into standalone executable programs. 2 | ;; It can be considered "downstream" of the rest of the compiler and is somewhat 3 | ;; independent. 4 | 5 | ;; based on https://github.com/ers35/luastatic/ 6 | (local fennel (require :fennel)) 7 | (local {: warn : copy} (require :fennel.utils)) 8 | 9 | (fn shellout [command] 10 | (let [f (io.popen command) 11 | stdout (f:read :*all)] 12 | (and (f:close) stdout))) 13 | 14 | (fn execute [cmd] 15 | (case (os.execute cmd) 16 | 0 true 17 | true true)) 18 | 19 | (fn string->c-hex-literal [characters] 20 | (table.concat (icollect [character (characters:gmatch ".")] 21 | (: "0x%02x" :format (string.byte character))) ", ")) 22 | 23 | (local c-shim "#ifdef __cplusplus 24 | extern \"C\" { 25 | #endif 26 | #include 27 | #include 28 | #include 29 | #ifdef __cplusplus 30 | } 31 | #endif 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #if LUA_VERSION_NUM == 501 38 | #define LUA_OK 0 39 | #endif 40 | 41 | /* Copied from lua.c */ 42 | 43 | static lua_State *globalL = NULL; 44 | 45 | static void lstop (lua_State *L, lua_Debug *ar) { 46 | (void)ar; /* unused arg. */ 47 | lua_sethook(L, NULL, 0, 0); /* reset hook */ 48 | luaL_error(L, \"interrupted!\"); 49 | } 50 | 51 | static void laction (int i) { 52 | signal(i, SIG_DFL); /* if another SIGINT happens, terminate process */ 53 | lua_sethook(globalL, lstop, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1); 54 | } 55 | 56 | static void createargtable (lua_State *L, char **argv, int argc, int script) { 57 | int i, narg; 58 | if (script == argc) script = 0; /* no script name? */ 59 | narg = argc - (script + 1); /* number of positive indices */ 60 | lua_createtable(L, narg, script + 1); 61 | for (i = 0; i < argc; i++) { 62 | lua_pushstring(L, argv[i]); 63 | lua_rawseti(L, -2, i - script); 64 | } 65 | lua_setglobal(L, \"arg\"); 66 | } 67 | 68 | static int msghandler (lua_State *L) { 69 | const char *msg = lua_tostring(L, 1); 70 | if (msg == NULL) { /* is error object not a string? */ 71 | if (luaL_callmeta(L, 1, \"__tostring\") && /* does it have a metamethod */ 72 | lua_type(L, -1) == LUA_TSTRING) /* that produces a string? */ 73 | return 1; /* that is the message */ 74 | else 75 | msg = lua_pushfstring(L, \"(error object is a %%s value)\", 76 | luaL_typename(L, 1)); 77 | } 78 | /* Call debug.traceback() instead of luaL_traceback() for Lua 5.1 compat. */ 79 | lua_getglobal(L, \"debug\"); 80 | lua_getfield(L, -1, \"traceback\"); 81 | /* debug */ 82 | lua_remove(L, -2); 83 | lua_pushstring(L, msg); 84 | /* original msg */ 85 | lua_remove(L, -3); 86 | lua_pushinteger(L, 2); /* skip this function and traceback */ 87 | lua_call(L, 2, 1); /* call debug.traceback */ 88 | return 1; /* return the traceback */ 89 | } 90 | 91 | static int docall (lua_State *L, int narg, int nres) { 92 | int status; 93 | int base = lua_gettop(L) - narg; /* function index */ 94 | lua_pushcfunction(L, msghandler); /* push message handler */ 95 | lua_insert(L, base); /* put it under function and args */ 96 | globalL = L; /* to be available to 'laction' */ 97 | signal(SIGINT, laction); /* set C-signal handler */ 98 | status = lua_pcall(L, narg, nres, base); 99 | signal(SIGINT, SIG_DFL); /* reset C-signal handler */ 100 | lua_remove(L, base); /* remove message handler from the stack */ 101 | return status; 102 | } 103 | 104 | int main(int argc, char *argv[]) { 105 | lua_State *L = luaL_newstate(); 106 | luaL_openlibs(L); 107 | createargtable(L, argv, argc, 0); 108 | 109 | static const unsigned char lua_loader_program[] = { 110 | %s 111 | }; 112 | if(luaL_loadbuffer(L, (const char*)lua_loader_program, 113 | sizeof(lua_loader_program), \"%s\") != LUA_OK) { 114 | fprintf(stderr, \"luaL_loadbuffer: %%s\\n\", lua_tostring(L, -1)); 115 | lua_close(L); 116 | return 1; 117 | } 118 | 119 | /* lua_bundle */ 120 | lua_newtable(L); 121 | static const unsigned char lua_require_1[] = { 122 | %s 123 | }; 124 | lua_pushlstring(L, (const char*)lua_require_1, sizeof(lua_require_1)); 125 | lua_setfield(L, -2, \"%s\"); 126 | 127 | %s 128 | 129 | if (docall(L, 1, LUA_MULTRET)) { 130 | const char *errmsg = lua_tostring(L, 1); 131 | if (errmsg) { 132 | fprintf(stderr, \"%%s\\n\", errmsg); 133 | } 134 | lua_close(L); 135 | return 1; 136 | } 137 | lua_close(L); 138 | return 0; 139 | }") 140 | 141 | (macro loader [] 142 | `(do 143 | (local bundle# ...) 144 | (fn loader# [name#] 145 | (case (or (. bundle# name#) (. bundle# (.. name# :.init))) 146 | (mod# ? (= :function (type mod#))) mod# 147 | (mod# ? (= :string (type mod#))) (assert (if (= _VERSION "Lua 5.1") 148 | (loadstring mod# name#) 149 | (load mod# name#))) 150 | nil (values nil (: "\n\tmodule '%%s' not found in fennel bundle" 151 | :format name#)))) 152 | 153 | (table.insert (or package.loaders package.searchers) 2 loader#) 154 | ((assert (loader# "%s")) ((or unpack table.unpack) arg)))) 155 | 156 | (fn compile-fennel [filename options] 157 | (let [f (if (= filename "-") 158 | io.stdin 159 | (assert (io.open filename :rb))) 160 | lua-code (fennel.compile-string (f:read :*a) options)] 161 | (f:close) 162 | lua-code)) 163 | 164 | (fn module-name [open rename used-renames] 165 | (let [require-name (case (. rename open) 166 | renamed (do 167 | (tset used-renames open true) 168 | renamed) 169 | _ open)] 170 | ;; changing initial underscore breaks luaossl 171 | (.. (require-name:sub 1 1) 172 | (: (require-name:sub 2) :gsub "_" ".")))) 173 | 174 | (fn native-loader [native ?options] 175 | (let [opts (or ?options {:rename-modules {}}) 176 | rename (or opts.rename-modules {}) 177 | used-renames {} 178 | nm (or (os.getenv :NM) :nm) 179 | out [" /* native libraries */"]] 180 | (each [_ path (ipairs native)] 181 | (let [opens []] 182 | (each [open (: (shellout (.. nm " " path)) :gmatch 183 | "[^dDt] _?luaopen_([%a%p%d]+)")] 184 | (table.insert opens open)) 185 | (when (= nil (. opens 1)) 186 | (warn (: (.. "Native module %s did not contain any luaopen_* symbols. " 187 | "Did you mean to use --native-library instead of --native-module?") 188 | :format path))) 189 | (each [_ open (ipairs opens)] 190 | (table.insert out (: " int luaopen_%s(lua_State *L);" :format open)) 191 | (table.insert out (: " lua_pushcfunction(L, luaopen_%s);" :format open)) 192 | (table.insert out (: " lua_setfield(L, -2, \"%s\");\n" :format 193 | (module-name open rename used-renames)))))) 194 | (each [key val (pairs rename)] 195 | (when (not (. used-renames key)) 196 | (warn (: (.. "unused --rename-native-module %s %s argument. " 197 | "Did you mean to include a native module?") 198 | :format key val)))) 199 | (table.concat out "\n"))) 200 | 201 | (fn fennel->c [filename native options] 202 | (let [basename (filename:gsub "(.*[\\/])(.*)" "%2") 203 | basename-noextension (or (basename:match "(.+)%.") basename) 204 | dotpath (-> filename 205 | (: :gsub "^%.%/" "") 206 | (: :gsub "[\\/]" ".")) 207 | dotpath-noextension (or (dotpath:match "(.+)%.") dotpath) 208 | fennel-loader (: (macrodebug (loader) :do) :format dotpath-noextension) 209 | lua-loader (fennel.compile-string fennel-loader) 210 | {: rename-modules} options] 211 | (c-shim:format (string->c-hex-literal lua-loader) basename-noextension 212 | (string->c-hex-literal (compile-fennel filename options)) 213 | dotpath-noextension (native-loader native {: rename-modules})))) 214 | 215 | (fn write-c [filename native options] 216 | (let [out-filename (.. filename :_binary.c) 217 | f (assert (io.open out-filename :w+))] 218 | (f:write (fennel->c filename native options)) 219 | (f:close) 220 | out-filename)) 221 | 222 | (fn compile-binary [lua-c-path 223 | executable-name 224 | static-lua 225 | lua-include-dir 226 | native] 227 | (let [cc (or (os.getenv :CC) :cc) 228 | ;; http://lua-users.org/lists/lua-l/2009-05/msg00147.html 229 | (rdynamic bin-extension ldl?) (if (-?> (shellout (.. cc " -dumpmachine")) 230 | (: :match :mingw)) 231 | (values "" :.exe false) 232 | (values :-rdynamic "" true)) 233 | compile-command [cc :-Os ; optimize for size 234 | lua-c-path (table.concat native " ") 235 | static-lua rdynamic 236 | :-lm (if ldl? :-ldl "") 237 | :-o (.. executable-name bin-extension) 238 | :-I lua-include-dir (os.getenv :CC_OPTS)]] 239 | (when (os.getenv :FENNEL_DEBUG) 240 | (print "Compiling with" (table.concat compile-command " "))) 241 | (when (not (execute (table.concat compile-command " "))) 242 | (print "Failed:" (table.concat compile-command " ")) 243 | (print "Ensure CC is set to the C compiler you intend to use.") 244 | (os.exit 1)) 245 | (when (not (os.getenv :FENNEL_DEBUG)) 246 | (os.remove lua-c-path)) 247 | (os.exit 0))) 248 | 249 | (fn native-path? [path] 250 | (let [(extension version-extension) (path:match "%.(%a+)(%.?%d*)$")] 251 | (if (and version-extension (not= version-extension "") 252 | (not (version-extension:match "%.%d+"))) false 253 | (case extension 254 | :a path 255 | :o path 256 | :so path 257 | :dylib path 258 | _ false)))) 259 | 260 | (fn extract-native-args [args] 261 | ;; all native libraries go in libraries; those with lua code go in modules too 262 | (let [native {:modules [] :libraries [] :rename-modules {}}] 263 | (for [i (length args) 1 -1] 264 | (when (= :--native-module (. args i)) 265 | (let [path (assert (native-path? (table.remove args (+ i 1))))] 266 | (table.insert native.modules 1 path) 267 | (table.insert native.libraries 1 path) 268 | (table.remove args i))) 269 | (when (= :--native-library (. args i)) 270 | (table.insert native.libraries 1 271 | (assert (native-path? (table.remove args (+ i 1))))) 272 | (table.remove args i)) 273 | (when (= :--rename-native-module (. args i)) 274 | (let [original (table.remove args (+ i 1)) 275 | new (table.remove args (+ i 1))] 276 | (tset native.rename-modules original new) 277 | (table.remove args i)))) 278 | (when (next args) 279 | (print (table.concat args " ")) 280 | (error (.. "Unknown args: " (table.concat args " ")))) 281 | native)) 282 | 283 | (fn compile [filename executable-name static-lua lua-include-dir options args] 284 | (let [{: modules : libraries : rename-modules} (extract-native-args args) 285 | opts {: rename-modules}] 286 | (copy options opts) 287 | (compile-binary (write-c filename modules opts) executable-name static-lua 288 | lua-include-dir libraries))) 289 | 290 | (local help " 291 | Usage: %s --compile-binary FILE OUT STATIC_LUA_LIB LUA_INCLUDE_DIR 292 | 293 | Compile a binary from your Fennel program. 294 | 295 | Requires a C compiler, a copy of liblua, and Lua's dev headers. Implies 296 | the --require-as-include option. 297 | 298 | FILE: the Fennel source being compiled. 299 | OUT: the name of the executable to generate 300 | STATIC_LUA_LIB: the path to the Lua library to use in the executable 301 | LUA_INCLUDE_DIR: the path to the directory of Lua C header files 302 | 303 | For example, on a Debian system, to compile a file called program.fnl using 304 | Lua 5.3, you would use this: 305 | 306 | $ %s --compile-binary program.fnl program \\ 307 | /usr/lib/x86_64-linux-gnu/liblua5.3.a /usr/include/lua5.3 308 | 309 | The program will be compiled to Lua, then compiled to C, then compiled to 310 | machine code. You can set the CC environment variable to change the compiler 311 | used (default: cc) or set CC_OPTS to pass in compiler options. For example 312 | set CC_OPTS=-static to generate a binary with static linking. 313 | 314 | This method is currently limited to programs do not transitively require Lua 315 | modules. Requiring a Lua module directly will work, but requiring a Lua module 316 | which requires another will fail. 317 | 318 | To include C libraries that contain Lua modules, add --native-module path/to.so, 319 | and to include C libraries without modules, use --native-library path/to.so. 320 | These options are unstable, barely tested, and even more likely to break. 321 | 322 | If you need to change the require name that a given native module is referenced 323 | as, you can use the --rename-native-module ORIGINAL NEW. ORIGINAL should be the 324 | suffix of the luaopen_* symbol in the native module. NEW should be the string 325 | you wish to pass to require to require the given native module. This can be used 326 | to handle cases where the name of an object file does not match the name of the 327 | luaopen_* symbol(s) within it. For example, the Lua readline bindings include a 328 | readline.lua file which is usually required as \"readline\", and a C-readline.so 329 | file which is required in the Lua half of the bindings like so: 330 | 331 | require 'C-readline' 332 | 333 | However, the symbol within the C-readline.so file is named luaopen_readline, so 334 | by default --compile-binary will make it so you can require it as \"readline\", 335 | which collides with the name of the readline.lua file and doesn't match the 336 | require call within readline.lua. In order to include the module within your 337 | compiled binary and have it get picked up by readline.lua correctly, you can 338 | specify the name used to refer to it in a require call by compiling it like 339 | so (this is assuming that program.fnl requires the Lua bindings): 340 | 341 | $ %s --compile-binary program.fnl program \\ 342 | /usr/lib/x86_64-linux-gnu/liblua5.3.a /usr/include/lua5.3 \\ 343 | --native-module C-readline.so \\ 344 | --rename-native-module readline C-readline 345 | ") 346 | 347 | {: compile : help} 348 | -------------------------------------------------------------------------------- /src/fennel/friend.fnl: -------------------------------------------------------------------------------- 1 | ;; This module contains functions that handle errors during parsing and 2 | ;; compilation and attempt to enrich them by suggesting fixes. 3 | ;; It can be disabled to fall back to the regular terse errors. 4 | 5 | (local {: unpack &as utils} (require :fennel.utils)) 6 | (local (utf8-ok? utf8) (pcall require :utf8)) 7 | 8 | (local suggestions {}) 9 | 10 | (fn pal [k v] 11 | (tset suggestions k v)) 12 | 13 | (pal "$ and $... in hashfn are mutually exclusive" 14 | ["modifying the hashfn so it only contains $... or $, $1, $2, $3, etc"]) 15 | 16 | (pal "can't introduce (.*) here" 17 | ["declaring the local at the top-level"]) 18 | 19 | (pal "can't start multisym segment with a digit" 20 | ["removing the digit" "adding a non-digit before the digit"]) 21 | 22 | (pal "cannot call literal value" 23 | ["checking for typos" 24 | "checking for a missing function name" 25 | "making sure to use prefix operators, not infix"]) 26 | 27 | (pal "could not compile value of type " 28 | ["debugging the macro you're calling to return a list or table"]) 29 | 30 | (pal "could not read number (.*)" 31 | ["removing the non-digit character" 32 | "beginning the identifier with a non-digit if it is not meant to be a number"]) 33 | 34 | (pal "expected a function.* to call" 35 | ["removing the empty parentheses" 36 | "using square brackets if you want an empty table"]) 37 | 38 | (pal "expected at least one pattern/body pair" 39 | ["adding a pattern and a body to execute when the pattern matches"]) 40 | 41 | (pal "expected binding and iterator" 42 | ["making sure you haven't omitted a local name or iterator"]) 43 | 44 | (pal "expected binding sequence" 45 | ["placing a table here in square brackets containing identifiers to bind"]) 46 | 47 | (pal "expected body expression" 48 | ["putting some code in the body of this form after the bindings"]) 49 | 50 | (pal "expected each macro to be function" 51 | ["ensuring that the value for each key in your macros table contains a function" 52 | "avoid defining nested macro tables"]) 53 | 54 | (pal "expected even number of name/value bindings" 55 | ["finding where the identifier or value is missing"]) 56 | 57 | (pal "expected even number of pattern/body pairs" 58 | ["checking that every pattern has a body to go with it" 59 | "adding _ before the final body"]) 60 | 61 | (pal "expected even number of values in table literal" 62 | ["removing a key" 63 | "adding a value"]) 64 | 65 | (pal "expected local" 66 | ["looking for a typo" 67 | "looking for a local which is used out of its scope"]) 68 | 69 | (pal "expected macros to be table" 70 | ["ensuring your macro definitions return a table"]) 71 | 72 | (pal "expected parameters" 73 | ["adding function parameters as a list of identifiers in brackets"]) 74 | 75 | (pal "expected range to include start and stop" 76 | ["adding missing arguments"]) 77 | 78 | (pal "expected rest argument before last parameter" 79 | ["moving & to right before the final identifier when destructuring"]) 80 | 81 | (pal "expected symbol for function parameter: (.*)" 82 | ["changing %s to an identifier instead of a literal value"]) 83 | 84 | (pal "expected var (.*)" 85 | ["declaring %s using var instead of let/local" 86 | "introducing a new local instead of changing the value of %s"]) 87 | 88 | (pal "expected vararg as last parameter" 89 | ["moving the \"...\" to the end of the parameter list"]) 90 | 91 | (pal "expected whitespace before opening delimiter" 92 | ["adding whitespace"]) 93 | 94 | (pal "global (.*) conflicts with local" 95 | ["renaming local %s"]) 96 | 97 | (pal "invalid character: (.)" 98 | ["deleting or replacing %s" 99 | "avoiding reserved characters like \", \\, ', ~, ;, @, `, and comma"]) 100 | 101 | (pal "local (.*) was overshadowed by a special form or macro" 102 | ["renaming local %s"]) 103 | 104 | (pal "macro not found in macro module" 105 | ["checking the keys of the imported macro module's returned table"]) 106 | 107 | (pal "macro tried to bind (.*) without gensym" 108 | ["changing to %s# when introducing identifiers inside macros"]) 109 | 110 | (pal "malformed multisym" 111 | ["ensuring each period or colon is not followed by another period or colon"]) 112 | 113 | (pal "may only be used at compile time" 114 | ["moving this to inside a macro if you need to manipulate symbols/lists" 115 | "using square brackets instead of parens to construct a table"]) 116 | 117 | (pal "method must be last component" 118 | ["using a period instead of a colon for field access" 119 | "removing segments after the colon" 120 | "making the method call, then looking up the field on the result"]) 121 | 122 | (pal "mismatched closing delimiter (.), expected (.)" 123 | ["replacing %s with %s" 124 | "deleting %s" "adding matching opening delimiter earlier"]) 125 | 126 | (pal "missing subject" 127 | ["adding an item to operate on"]) 128 | 129 | (pal "multisym method calls may only be in call position" 130 | ["using a period instead of a colon to reference a table's fields" 131 | "putting parens around this"]) 132 | 133 | (pal "tried to reference a macro without calling it" 134 | ["renaming the macro so as not to conflict with locals"]) 135 | 136 | (pal "tried to reference a special form without calling it" 137 | ["making sure to use prefix operators, not infix" 138 | "wrapping the special in a function if you need it to be first class"]) 139 | 140 | (pal "tried to use unquote outside quote" 141 | ["moving the form to inside a quoted form" 142 | "removing the comma"]) 143 | 144 | (pal "tried to use vararg with operator" 145 | ["accumulating over the operands"]) 146 | 147 | (pal "unable to bind (.*)" 148 | ["replacing the %s with an identifier"]) 149 | 150 | (pal "unexpected arguments" 151 | ["removing an argument" 152 | "checking for typos"]) 153 | 154 | (pal "unexpected closing delimiter (.)" 155 | ["deleting %s" 156 | "adding matching opening delimiter earlier"]) 157 | 158 | (pal "unexpected iterator clause" 159 | ["removing an argument" 160 | "checking for typos"]) 161 | 162 | (pal "unexpected multi symbol (.*)" 163 | ["removing periods or colons from %s"]) 164 | 165 | (pal "unexpected vararg" 166 | ["putting \"...\" at the end of the fn parameters if the vararg was intended"]) 167 | 168 | (pal "unknown identifier: (.*)" 169 | ["looking to see if there's a typo" 170 | "using the _G table instead, eg. _G.%s if you really want a global" 171 | "moving this code to somewhere that %s is in scope" 172 | "binding %s as a local in the scope of this code"]) 173 | 174 | (pal "unused local (.*)" 175 | ["renaming the local to _%s if it is meant to be unused" 176 | "fixing a typo so %s is used" 177 | "disabling the linter which checks for unused locals"]) 178 | 179 | (pal "use of global (.*) is aliased by a local" 180 | ["renaming local %s" 181 | "refer to the global using _G.%s instead of directly"]) 182 | 183 | (fn suggest [msg] 184 | (accumulate [s nil pat sug (pairs suggestions) :until s] 185 | (let [matches [(msg:match pat)]] 186 | (when (next matches) 187 | (icollect [_ s (ipairs sug)] 188 | (s:format (unpack matches))))))) 189 | 190 | (fn read-line [filename line ?source] 191 | (if ?source 192 | (let [matcher (string.gmatch (.. ?source "\n") "(.-)(\r?\n)")] 193 | (for [_ 2 line] (matcher)) 194 | (matcher)) 195 | (with-open [f (assert (_G.io.open filename))] 196 | (for [_ 2 line] (f:read)) 197 | (f:read)))) 198 | 199 | (fn sub [str start end] 200 | "Try to take the substring based on characters, not bytes." 201 | (if (or (< end start) (< (length str) start)) "" 202 | utf8-ok? 203 | (string.sub str (utf8.offset str start) 204 | (- (or (utf8.offset str (+ end 1)) (+ (utf8.len str) 1)) 1)) 205 | (string.sub str start (math.min end (str:len))))) 206 | 207 | (fn highlight-line [codeline col ?endcol opts] 208 | (if (or (and opts (= false opts.error-pinpoint)) 209 | (and os os.getenv (os.getenv "NO_COLOR"))) 210 | codeline 211 | (let [{: error-pinpoint} (or opts {}) 212 | endcol (or ?endcol col) 213 | eol (if utf8-ok? (utf8.len codeline) (string.len codeline)) 214 | [open close] (or error-pinpoint ["\027[7m" "\027[0m"])] 215 | (.. (sub codeline 1 col) open 216 | (sub codeline (+ col 1) (+ endcol 1)) 217 | close (sub codeline (+ endcol 2) eol))))) 218 | 219 | (fn friendly-msg [msg {: filename : line : col : endcol : endline} source opts] 220 | (let [(ok codeline) (pcall read-line filename line source) 221 | endcol (if (and ok codeline (not= line endline)) 222 | (length codeline) 223 | endcol) 224 | out [msg ""]] 225 | ;; don't assume the file can be read as-is 226 | ;; (when (not ok) (print :err codeline)) 227 | (when (and ok codeline) 228 | (if col 229 | (table.insert out (highlight-line codeline col endcol opts)) 230 | (table.insert out codeline))) 231 | (each [_ suggestion (ipairs (or (suggest msg) []))] 232 | (table.insert out (: "* Try %s." :format suggestion))) 233 | (table.concat out "\n"))) 234 | 235 | (fn assert-compile [condition msg ast source opts] 236 | "A drop-in replacement for the internal assert-compile with friendly messages." 237 | (when (not condition) 238 | (let [{: filename : line : col} (utils.ast-source ast)] 239 | (error (friendly-msg (: "%s:%s:%s: Compile error: %s" :format 240 | ;; still need fallbacks because backtick erases 241 | ;; source and macros can generate source-less ast 242 | (or filename :unknown) (or line "?") 243 | (or col "?") msg) 244 | (utils.ast-source ast) source opts) 0))) 245 | condition) 246 | 247 | (fn parse-error [msg filename line col endcol source opts] 248 | "A drop-in replacement for the internal parse-error with friendly messages." 249 | (error (friendly-msg (: "%s:%s:%s: Parse error: %s" :format 250 | filename line col msg) 251 | {: filename : line : col : endcol :endline line} 252 | source opts) 0)) 253 | 254 | {: assert-compile : parse-error} 255 | -------------------------------------------------------------------------------- /src/launcher.fnl: -------------------------------------------------------------------------------- 1 | ;; This is the command-line entry point for Fennel. 2 | 3 | (local fennel (require :fennel)) 4 | (local {: pack : unpack} (require :fennel.utils)) 5 | 6 | (local help "Usage: fennel [FLAG] [FILE] 7 | 8 | Run Fennel, a Lisp programming language for the Lua runtime. 9 | 10 | --repl : Command to launch an interactive REPL session 11 | --compile FILES (-c) : Command to AOT compile files, writing Lua to stdout 12 | --eval SOURCE (-e) : Command to evaluate source code and print result 13 | 14 | --correlate : Make Lua output line numbers try to match Fennel's 15 | --load FILE (-l) : Load the specified FILE before executing command 16 | --no-compiler-sandbox : Don't limit compiler environment to minimal sandbox 17 | --compile-binary FILE 18 | OUT LUA_LIB LUA_DIR : Compile FILE to standalone binary OUT 19 | --compile-binary --help : Display further help for compiling binaries 20 | --add-package-path PATH : Add PATH to package.path for finding Lua modules 21 | --add-package-cpath PATH : Add PATH to package.cpath for finding Lua modules 22 | --add-fennel-path PATH : Add PATH to fennel.path for finding Fennel modules 23 | --add-macro-path PATH : Add PATH to fennel.macro-path for macro modules 24 | --globals G1[,G2...] : Allow these globals in addition to standard ones 25 | --globals-only G1[,G2] : Same as above, but exclude standard ones 26 | --assert-as-repl : Replace assert calls with assert-repl 27 | --require-as-include : Inline required modules in the output 28 | --skip-include M1[,M2] : Omit certain modules from output when included 29 | --use-bit-lib : Use LuaJITs bit library instead of operators 30 | --metadata : Enable function metadata, even in compiled output 31 | --no-metadata : Disable function metadata, even in REPL 32 | --lua LUA_EXE : Run in a child process with LUA_EXE 33 | --plugin FILE : Activate the compiler plugin in FILE 34 | --raw-errors : Disable friendly compile error reporting 35 | --no-searcher : Skip installing package.searchers entry 36 | --no-fennelrc : Skip loading ~/.fennelrc when launching REPL 37 | --keywords K1[,K2...] : Treat these symbols as reserved Lua keywords 38 | 39 | --help (-h) : Display this text 40 | --version (-v) : Show version 41 | 42 | Globals are not checked when doing AOT (ahead-of-time) compilation unless 43 | the --globals-only or --globals flag is provided. Use --globals \"*\" to disable 44 | strict globals checking in other contexts. 45 | 46 | Metadata is typically considered a development feature and is not recommended 47 | for production. It is used for docstrings and enabled by default in the REPL. 48 | 49 | When not given a command, runs the file given as the first argument. 50 | When given neither command nor file, launches a REPL. 51 | 52 | Use the NO_COLOR environment variable to disable escape codes in error messages. 53 | 54 | If ~/.fennelrc exists, it will be loaded before launching a REPL.") 55 | 56 | (local options {:plugins [] :keywords {}}) 57 | 58 | (fn dosafely [f ...] 59 | (let [args [...] 60 | result (pack (xpcall #(f (unpack args)) fennel.traceback))] 61 | (when (not (. result 1)) 62 | (io.stderr:write (.. (tostring (. result 2)) "\n")) 63 | (os.exit 1)) 64 | (unpack result 2 result.n))) 65 | 66 | (fn allow-globals [names actual-globals] 67 | (if (= names "*") 68 | (set options.allowedGlobals false) 69 | (do 70 | (set options.allowedGlobals (icollect [g (names:gmatch "([^,]+),?")] g)) 71 | (each [global-name (pairs actual-globals)] 72 | (table.insert options.allowedGlobals global-name))))) 73 | 74 | (fn handle-load [i] 75 | (let [file (table.remove arg (+ i 1))] 76 | (dosafely fennel.dofile file options) 77 | (table.remove arg i))) 78 | 79 | (fn handle-lua [i] 80 | (table.remove arg i) ; remove the --lua flag from args 81 | (let [tgt-lua (table.remove arg i) 82 | cmd [(string.format "%s %s" tgt-lua (or (. arg 0) "fennel"))]] 83 | (for [i 1 (length arg)] ; quote args to prevent shell escapes when executing 84 | (table.insert cmd (string.format "%q" (. arg i)))) 85 | (when (= nil (. arg -1)) 86 | (io.stderr:write 87 | "WARNING: --lua argument only works from script, not binary.\n")) 88 | (case (os.execute (table.concat cmd " ")) 89 | (where (or (true :exit) 0)) (os.exit 0 true) 90 | _ (os.exit 1 true)))) 91 | 92 | ;; check for --lua first to ensure its child process retains all flags 93 | (for [i (length arg) 1 -1] 94 | (case (. arg i) 95 | :--lua (handle-lua i))) 96 | 97 | (fn load-plugin [filename] 98 | (let [opts {:env :_COMPILER :compiler-env _G :useMetadata true}] 99 | (if (= ".lua" (filename:sub -4)) 100 | ((fennel.load-code (: (assert (io.open filename :rb)) :read :*a) 101 | ((. (require :fennel.specials) :make-compiler-env) 102 | nil (fennel.scope) nil opts) 103 | filename)) 104 | (fennel.dofile filename opts)))) 105 | 106 | (let [commands {:--repl true 107 | :--compile true :-c true 108 | :--compile-binary true 109 | :--eval true :-e true 110 | :--version true :-v true 111 | :--help true :-h true 112 | "-" true}] 113 | (var i 1) 114 | (while (and (. arg i) (not options.ignore-options)) 115 | (case (. arg i) 116 | :--no-searcher (do 117 | (set options.no-searcher true) 118 | (table.remove arg i)) 119 | :--indent (do 120 | (set options.indent (table.remove arg (+ i 1))) 121 | (when (= options.indent :false) 122 | (set options.indent false)) 123 | (table.remove arg i)) 124 | :--add-package-path (let [entry (table.remove arg (+ i 1))] 125 | (set package.path (.. entry ";" package.path)) 126 | (table.remove arg i)) 127 | :--add-package-cpath (let [entry (table.remove arg (+ i 1))] 128 | (set package.cpath (.. entry ";" package.cpath)) 129 | (table.remove arg i)) 130 | :--add-fennel-path (let [entry (table.remove arg (+ i 1))] 131 | (set fennel.path (.. entry ";" fennel.path)) 132 | (table.remove arg i)) 133 | :--add-macro-path (let [entry (table.remove arg (+ i 1))] 134 | (set fennel.macro-path (.. entry ";" fennel.macro-path)) 135 | (table.remove arg i)) 136 | :--load (handle-load i) 137 | :-l (handle-load i) 138 | :--no-fennelrc (do 139 | (set options.fennelrc false) 140 | (table.remove arg i)) 141 | :--correlate (do 142 | (set options.correlate true) 143 | (table.remove arg i)) 144 | :--globals (do 145 | (allow-globals (table.remove arg (+ i 1)) _G) 146 | (table.remove arg i)) 147 | :--globals-only (do 148 | (allow-globals (table.remove arg (+ i 1)) {}) 149 | (table.remove arg i)) 150 | :--require-as-include (do 151 | (set options.requireAsInclude true) 152 | (table.remove arg i)) 153 | :--assert-as-repl (do 154 | (set options.assertAsRepl true) 155 | (table.remove arg i)) 156 | :--skip-include (let [skip-names (table.remove arg (+ i 1)) 157 | skip (icollect [m (skip-names:gmatch "([^,]+)")] m)] 158 | (set options.skipInclude skip) 159 | (table.remove arg i)) 160 | :--use-bit-lib (do 161 | (set options.useBitLib true) 162 | (table.remove arg i)) 163 | :--metadata (do 164 | (set options.useMetadata true) 165 | (table.remove arg i)) 166 | :--no-metadata (do 167 | (set options.useMetadata false) 168 | (table.remove arg i)) 169 | :--no-compiler-sandbox (do 170 | (set options.compiler-env _G) 171 | (table.remove arg i)) 172 | :--raw-errors (do 173 | (set options.unfriendly true) 174 | (table.remove arg i)) 175 | :--plugin (let [plugin (load-plugin (table.remove arg (+ i 1)))] 176 | (table.insert options.plugins 1 plugin) 177 | (table.remove arg i)) 178 | :--keywords (do 179 | (each [keyword (string.gmatch (table.remove arg (+ i 1)) "[^,]+")] 180 | (tset options.keywords keyword true)) 181 | (table.remove arg i)) 182 | _ (do 183 | (when (not (. commands (. arg i))) 184 | (set options.ignore-options true) 185 | (set i (+ i 1))) 186 | (set i (+ i 1)))))) 187 | 188 | (local searcher-opts {}) 189 | 190 | (when (not options.no-searcher) 191 | (each [k v (pairs options)] 192 | (tset searcher-opts k v)) 193 | (table.insert (or package.loaders package.searchers) 194 | (fennel.make-searcher searcher-opts))) 195 | 196 | (fn load-initfile [] 197 | (let [home (or (os.getenv :HOME) "/") 198 | xdg-config-home (or (os.getenv :XDG_CONFIG_HOME) (.. home :/.config)) 199 | xdg-initfile (.. xdg-config-home :/fennel/fennelrc) 200 | home-initfile (.. home :/.fennelrc) 201 | init (io.open xdg-initfile :rb) 202 | init-filename (if init xdg-initfile home-initfile) 203 | init (or init (io.open home-initfile :rb))] 204 | (when init 205 | (init:close) 206 | (dosafely fennel.dofile init-filename options options fennel)))) 207 | 208 | (fn repl [] 209 | (let [readline? (and (not= "dumb" (os.getenv "TERM")) 210 | (pcall require :readline)) 211 | welcome [(.. "Welcome to " (fennel.runtime-version) "!") 212 | "Use ,help to see available commands."]] 213 | (set searcher-opts.useMetadata (not= false options.useMetadata)) 214 | (when (not= false options.fennelrc) 215 | (set options.fennelrc load-initfile)) 216 | (when (and (not readline?) (not= "dumb" (os.getenv "TERM"))) 217 | (table.insert welcome (.. "Try installing readline via luarocks for a " 218 | "better repl experience."))) 219 | (set options.message (table.concat welcome "\n")) 220 | (fennel.repl options))) 221 | 222 | (fn eval [form] 223 | (print (dosafely fennel.eval (if (= form "-") 224 | (io.stdin:read :*a) 225 | form) options))) 226 | 227 | (fn compile [files] 228 | (each [_ filename (ipairs files)] 229 | (set options.filename filename) 230 | (let [f (if (= filename "-") 231 | io.stdin 232 | (assert (io.open filename :rb)))] 233 | (case (xpcall #(fennel.compile-string (f:read :*a) options) 234 | fennel.traceback) 235 | (true val) (print val) 236 | (_ msg) (do 237 | (io.stderr:write (.. msg "\n")) 238 | (os.exit 1))) 239 | (f:close)))) 240 | 241 | (match arg 242 | ([] ? (= 0 (length arg))) (repl) 243 | [:--repl] (repl) 244 | [:--compile & files] (compile files) 245 | [:-c & files] (compile files) 246 | [:--compile-binary filename out static-lua lua-include-dir & args] 247 | (let [bin (require :fennel.binary)] 248 | (set options.filename filename) 249 | (set options.requireAsInclude true) 250 | (bin.compile filename out static-lua lua-include-dir options args)) 251 | [:--compile-binary] (let [cmd (or (. arg 0) "fennel")] 252 | (print (: (. (require :fennel.binary) :help) 253 | :format cmd cmd cmd))) 254 | [:--eval form] (eval form) 255 | [:-e form] (eval form) 256 | ([a] ? (or (= a :-v) (= a :--version))) 257 | (print (fennel.runtime-version)) 258 | [:--help] (print help) 259 | [:-h] (print help) 260 | ["-"] (dosafely fennel.eval (io.stdin:read :*a)) 261 | [filename & args] (do 262 | (tset arg -2 (. arg -1)) 263 | (tset arg -1 (. arg 0)) 264 | (tset arg 0 (table.remove arg 1)) 265 | (dosafely fennel.dofile filename options (unpack args)))) 266 | -------------------------------------------------------------------------------- /src/linter.fnl: -------------------------------------------------------------------------------- 1 | ;; fennel-ls: macro-file 2 | ;; This file is provided as an example of Fennel's plugin API; it is 3 | ;; not part of Fennel's public API. If you want a real linter, use 4 | ;; fennel-ls. 5 | 6 | (fn set-set-meta [to scope opts] 7 | (when (not (or opts.declaration (multi-sym? to))) 8 | (if (sym? to) 9 | (tset scope.symmeta (tostring to) :set true) 10 | (each [_ sub (ipairs to)] 11 | (set-set-meta sub scope opts))))) 12 | 13 | (fn save-meta [from to scope opts] 14 | "When destructuring, save module name if local is bound to a `require' call. 15 | Doesn't do any linting on its own; just saves the data for other linters." 16 | (when (and (sym? to) (not (multi-sym? to)) (list? from) 17 | (sym? (. from 1)) (= :require (tostring (. from 1))) 18 | (= :string (type (. from 2)))) 19 | (let [meta (. scope.symmeta (tostring to))] 20 | (set meta.required (tostring (. from 2))))) 21 | (set-set-meta to scope opts)) 22 | 23 | (fn check-module-fields [symbol scope] 24 | "When referring to a field in a local that's a module, make sure it exists." 25 | (let [[module-local field] (or (multi-sym? symbol) []) 26 | module-name (-?> scope.symmeta (. (tostring module-local)) (. :required)) 27 | module (and module-name (require module-name))] 28 | (assert-compile (or (= module nil) (not= (. module field) nil)) 29 | (string.format "Missing field %s in module %s" 30 | (or field :?) (or module-name :?)) symbol))) 31 | 32 | (fn arity-check? [module module-name] 33 | (or (-?> module getmetatable (. :arity-check?)) 34 | (pcall debug.getlocal #nil 1) ; PUC 5.1 can't use debug.getlocal for this 35 | ;; I don't love this method of configuration but it gets the job done. 36 | (match (and module-name os os.getenv (os.getenv "FENNEL_LINT_MODULES")) 37 | module-pattern (module-name:find module-pattern)))) 38 | 39 | (fn descend [target [part & parts]] 40 | (if (= nil part) target 41 | (= :table (type target)) (match (. target part) 42 | new-target (descend new-target parts)) 43 | target)) 44 | 45 | (fn min-arity [target last-required name] 46 | (match (debug.getlocal target last-required) 47 | localname (if (and (localname:match "^_3f") (< 0 last-required)) 48 | (min-arity target (- last-required 1)) 49 | last-required) 50 | _ last-required)) 51 | 52 | (fn arity-check-call [[f & args] scope] 53 | "Perform static arity checks on static function calls in a module." 54 | (let [last-arg (. args (length args)) 55 | arity (if (: (tostring f) :find ":") ; method 56 | (+ (length args) 1) 57 | (length args)) 58 | [f-local & parts] (or (multi-sym? f) []) 59 | module-name (-?> scope.symmeta (. (tostring f-local)) (. :required)) 60 | module (and module-name (require module-name)) 61 | field (table.concat parts ".") 62 | target (descend module parts)] 63 | (when (and (arity-check? module module-name) _G.debug _G.debug.getinfo 64 | module (not (varg? last-arg)) (not (list? last-arg))) 65 | (assert-compile (= (type target) :function) 66 | (string.format "Missing function %s in module %s" 67 | (or field :?) module-name) f) 68 | (match (_G.debug.getinfo target) 69 | {: nparams :what "Lua"} 70 | (let [min (min-arity target nparams f)] 71 | (assert-compile (<= min arity) 72 | (: "Called %s with %s arguments, expected at least %s" 73 | :format f arity min) f)))))) 74 | 75 | (fn check-unused [ast scope] 76 | (each [symname (pairs scope.symmeta)] 77 | (let [meta (. scope.symmeta symname)] 78 | (assert-compile (or meta.used (symname:find "^_")) 79 | (string.format "unused local %s" (or symname :?)) ast) 80 | (assert-compile (or (not meta.var) meta.set) 81 | (string.format "%s declared as var but never set" 82 | symname) ast)))) 83 | 84 | {:destructure save-meta 85 | :symbol-to-expression check-module-fields 86 | :call arity-check-call 87 | ;; Note that this will only check unused args inside functions and let blocks, 88 | ;; not top-level locals of a chunk. 89 | :fn check-unused 90 | :do check-unused 91 | :chunk check-unused 92 | :name "fennel/linter" 93 | :versions "^1%."} 94 | -------------------------------------------------------------------------------- /test/api.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | 3 | (local expected { 4 | :comment "function" 5 | :comment? "function" 6 | :compile "function" 7 | :compile-stream "function" 8 | :compile-string "function" 9 | :doc "function" 10 | :dofile "function" 11 | :eval "function" 12 | :getinfo "function" 13 | :install "function" 14 | :list "function" 15 | :list? "function" 16 | :load-code "function" 17 | :macro-loaded "table" 18 | :macro-path "string" 19 | :macro-searchers "table" 20 | :make-searcher "function" 21 | :metadata "table" 22 | :multi-sym? "function" 23 | :parser "function" 24 | :path "string" 25 | :repl "callable" 26 | :runtime-version "function" 27 | :search-module "function" 28 | :searcher "function" 29 | :sequence "function" 30 | :sequence? "function" 31 | :sym "function" 32 | :sym-char? "function" 33 | :sym? "function" 34 | :syntax "function" 35 | :table? "function" 36 | :traceback "function" 37 | :varg "function" 38 | :varg? "function" 39 | :version "string" 40 | :view "function" 41 | :ast-source "function"}) 42 | 43 | (local expected-aliases { 44 | :compileStream "function" 45 | :compileString "function" 46 | :loadCode "function" 47 | :macroLoaded "table" 48 | :macroPath "string" 49 | :macroSearchers "table" 50 | :makeSearcher "function" 51 | :runtimeVersion "function" 52 | :searchModule "function"}) 53 | 54 | (local expected-deprecations { 55 | :compile1 "function" 56 | :gensym "function" 57 | :granulate "function" 58 | :make_searcher "function" 59 | :mangle "function" 60 | :scope "function" 61 | :string-stream "function" 62 | :stringStream "function" 63 | :unmangle "function"}) 64 | 65 | (fn supertype [expect v] 66 | (let [vt (type v)] 67 | (if (and (= expect :callable) 68 | (or (= vt :function) (and (= vt :table) 69 | (?. (getmetatable v) :__call)))) 70 | :callable 71 | vt))) 72 | 73 | (fn test-api-exposure [] 74 | (let [fennel (require :fennel) current {}] 75 | 76 | (each [key value (pairs fennel)] 77 | (tset current key (type value))) 78 | 79 | (each [key kind (pairs expected)] 80 | (t.is (. fennel key) (.. "expect fennel." key " to exists")) 81 | (t.= (supertype kind (. fennel key)) kind 82 | (.. "expect fennel." key " to be \"" kind "\""))) 83 | 84 | (each [key kind (pairs expected-aliases)] 85 | (t.is (. fennel key) (.. "expect alias fennel." key " to exists")) 86 | (t.= (supertype kind (. fennel key)) kind 87 | (.. "expect alias fennel." key " to be \"" kind "\""))) 88 | 89 | (each [key kind (pairs expected-deprecations)] 90 | (t.is (. fennel key) (.. "expect deprecated fennel." key " to exists")) 91 | (t.= (supertype kind (. fennel key)) kind 92 | (.. "expect deprecated fennel." key " to be \"" kind "\""))) 93 | 94 | (each [key value (pairs fennel)] 95 | (t.is (or (. expected key) 96 | (. expected-aliases key) 97 | (. expected-deprecations key)) 98 | (.. "fennel." key " not expected to be in the public api"))))) 99 | 100 | {: test-api-exposure} 101 | -------------------------------------------------------------------------------- /test/bad/all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # helper script to spit out all the suggestions covered by fennelfriend. 4 | 5 | for f in test/bad/*.fnl; do 6 | echo "============================================ $f" 7 | ./fennel $f || true 8 | done 9 | -------------------------------------------------------------------------------- /test/bad/call-literal.fnl: -------------------------------------------------------------------------------- 1 | (print (1 löl 6)) 2 | -------------------------------------------------------------------------------- /test/bad/expected-binding.fnl: -------------------------------------------------------------------------------- 1 | (let 12) ; this one is never going to get more precise 2 | -------------------------------------------------------------------------------- /test/bad/expected-body.fnl: -------------------------------------------------------------------------------- 1 | (let [x 1]) 2 | -------------------------------------------------------------------------------- /test/bad/expected-even-bindings.fnl: -------------------------------------------------------------------------------- 1 | (let [x 1 y] x) 2 | -------------------------------------------------------------------------------- /test/bad/expected-function.fnl: -------------------------------------------------------------------------------- 1 | (let [x ()] 2 | 1) 3 | -------------------------------------------------------------------------------- /test/bad/expected-local.fnl: -------------------------------------------------------------------------------- 1 | (set [a b c] [1 2 3]) 2 | -------------------------------------------------------------------------------- /test/bad/expected-parameters.fnl: -------------------------------------------------------------------------------- 1 | (fn myfun (a) a) 2 | -------------------------------------------------------------------------------- /test/bad/expected-rest.fnl: -------------------------------------------------------------------------------- 1 | (fn [[a & b c]] c) 2 | -------------------------------------------------------------------------------- /test/bad/expected-symbol-parameter.fnl: -------------------------------------------------------------------------------- 1 | (fn aöén [a 1 b] b) 2 | -------------------------------------------------------------------------------- /test/bad/global-alias.fnl: -------------------------------------------------------------------------------- 1 | (let [x-y 1] 2 | (global x_y 2)) 3 | -------------------------------------------------------------------------------- /test/bad/global-conflict.fnl: -------------------------------------------------------------------------------- 1 | (local x 1) 2 | (global x 2) 3 | -------------------------------------------------------------------------------- /test/bad/illegal-character.fnl: -------------------------------------------------------------------------------- 1 | (local xy@z 9) 2 | -------------------------------------------------------------------------------- /test/bad/macro-bind.fnl: -------------------------------------------------------------------------------- 1 | (macro abc [x] 2 | `(let [y 2] 3 | ,x)) 4 | 5 | (let [xyz 123] 6 | (abc xyz)) 7 | -------------------------------------------------------------------------------- /test/bad/macro-no-return-table.fnl: -------------------------------------------------------------------------------- 1 | (fn i-am-not-a-table [] "This won't work with import-macros" :oops) 2 | -------------------------------------------------------------------------------- /test/bad/mismatched-closing.fnl: -------------------------------------------------------------------------------- 1 | (local tbl [}]) 2 | -------------------------------------------------------------------------------- /test/bad/multisym-digit.fnl: -------------------------------------------------------------------------------- 1 | (print "无为" tbl.2keyed4you) 2 | -------------------------------------------------------------------------------- /test/bad/multisym-last.fnl: -------------------------------------------------------------------------------- 1 | (local abc []) 2 | 3 | (print (abc:def.xyz)) 4 | -------------------------------------------------------------------------------- /test/bad/multisym-malformed.fnl: -------------------------------------------------------------------------------- 1 | (print abc::def) 2 | -------------------------------------------------------------------------------- /test/bad/multisym-method.fnl: -------------------------------------------------------------------------------- 1 | (fn abc [] 2 | (print xyz:abc)) 3 | -------------------------------------------------------------------------------- /test/bad/no-whitespace-before-open.fnl: -------------------------------------------------------------------------------- 1 | (fn [a](+ a 2)) 2 | -------------------------------------------------------------------------------- /test/bad/numeric-token.fnl: -------------------------------------------------------------------------------- 1 | (local x 156-9) 2 | -------------------------------------------------------------------------------- /test/bad/odd-table.fnl: -------------------------------------------------------------------------------- 1 | (local f {:a 1 :b 2 :c}) 2 | -------------------------------------------------------------------------------- /test/bad/only-compile-time.fnl: -------------------------------------------------------------------------------- 1 | (let [x 1] 2 | `(print ,x)) 3 | -------------------------------------------------------------------------------- /test/bad/set-local.fnl: -------------------------------------------------------------------------------- 1 | (local x 1) 2 | (set x 3) 3 | -------------------------------------------------------------------------------- /test/bad/special-shadow.fnl: -------------------------------------------------------------------------------- 1 | (local + 1) 2 | -------------------------------------------------------------------------------- /test/bad/unable-to-bind-for.fnl: -------------------------------------------------------------------------------- 1 | (for [1 2 3] 2 | 6) 3 | -------------------------------------------------------------------------------- /test/bad/unable-to-bind.fnl: -------------------------------------------------------------------------------- 1 | (let [1 (+ -2 3)] 1) 2 | -------------------------------------------------------------------------------- /test/bad/unexpected-close-top.fnl: -------------------------------------------------------------------------------- 1 | (local x 1) ] 2 | -------------------------------------------------------------------------------- /test/bad/unexpected-vararg.fnl: -------------------------------------------------------------------------------- 1 | (fn [] 2 | (print ...)) 3 | -------------------------------------------------------------------------------- /test/bad/unknown-global.fnl: -------------------------------------------------------------------------------- 1 | (let [x y] 2 | (+ z 1)) 3 | -------------------------------------------------------------------------------- /test/bad/unused.fnl: -------------------------------------------------------------------------------- 1 | (let [x 1 y 2 z 3] 2 | (+ x z)) 3 | -------------------------------------------------------------------------------- /test/bad/vararg-not-last.fnl: -------------------------------------------------------------------------------- 1 | (fn [... a] a) 2 | -------------------------------------------------------------------------------- /test/bit.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | (local fennel (require :fennel)) 3 | 4 | (macro == [form expected] 5 | `(let [(ok# val#) (pcall fennel.eval ,(view form) 6 | {:useBitLib (not= nil _G.bit)})] 7 | (t.is ok# val#) 8 | (t.= ,expected val#))) 9 | 10 | (fn test-shifts [] 11 | (== (lshift 33 2) 132) 12 | (== (lshift 1) 2) 13 | (== (rshift 33 2) 8) 14 | (let [(ok? msg) (pcall fennel.compileString "(lshift)")] 15 | (t.is (not ok?)) 16 | (t.match "Expected more than 0 arguments" msg))) 17 | 18 | (fn test-ops [] 19 | ;; multiple args 20 | (== (band 0x16 0xd) 0x4) 21 | (== (band 0xff 0x91) 0x91) 22 | (== (bor 0x1 0x2 0x4 0x8) 0xf) 23 | (== (bxor 0x2 0xf0) 0xf2) 24 | ;; one arg 25 | (== (bxor 1) 1) 26 | (== (bor 0x33) 0x33) 27 | (== (band 0x93) 0x93) 28 | (== (bnot 26) -27) 29 | ;; no args 30 | (== (band) -1) 31 | (== (bor) 0) 32 | (== (bxor) 0)) 33 | 34 | ;; skip the test on PUC 5.1 and 5.2 35 | (if (or (rawget _G :jit) (not (_VERSION:find "5%.[12]"))) 36 | {: test-shifts 37 | : test-ops} 38 | {}) 39 | -------------------------------------------------------------------------------- /test/cli.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | 3 | (macro v [form] (view form)) 4 | 5 | ;; These are the slowest tests, so for now we just have a basic sanity check 6 | ;; to ensure that it compiles and can evaluate math. 7 | 8 | (local test-all? (os.getenv :FNL_TESTALL)) ; set by `make testall` 9 | 10 | (local host-lua (let [long (case _VERSION 11 | "Lua 5.1" (if _G.jit :luajit :lua5.1) 12 | _ (.. :lua (_VERSION:sub 5))) 13 | p (io.popen (.. long " -v"))] 14 | (p:read :*all) 15 | (if (p:close) 16 | long 17 | (or (os.getenv "LUA") "lua")))) 18 | 19 | (fn file-exists? [filename] 20 | (let [f (io.open filename)] 21 | (when f (f:close) true))) 22 | 23 | (λ sh/esc [str] (.. "'" (str:gsub "'" "'\\''") "'")) 24 | (λ fennel-cli [...] 25 | (let [cmd [host-lua :fennel ...] 26 | proc (io.popen (table.concat cmd " ")) 27 | output (: (proc:read :*a) :gsub "\n$" "")] 28 | (values (proc:close) output))) ; proc:close gives exit status on 5.2+ 29 | 30 | (λ peval [code ...] (fennel-cli :--eval (sh/esc code) ...)) 31 | 32 | (fn test-cli [] 33 | ;; skip this if we haven't compiled the CLI or on Windows 34 | (when (and (file-exists? "fennel") (= "/" (package.config:sub 1 1))) 35 | (t.= [true "1\tnil\t2\tnil\tnil"] 36 | [(peval (v (values 1 nil 2 nil nil)))]))) 37 | 38 | (fn test-lua-flag [] 39 | ;; skip this when cli is not compiled or not running tests with `make testall` 40 | (when (and test-all? (file-exists? "fennel")) 41 | (let [;; running io.popen for all 20 combinations of lua versions is slow, 42 | ;; so we'll just pick the next one in the list after host-lua 43 | lua-exec ((fn pick-lua [lua-vs i lua-v] 44 | (if (= host-lua lua-v) 45 | (. lua-vs (+ 1 (% i (# lua-vs)))) ; circular next 46 | (pick-lua lua-vs (next lua-vs i)))) 47 | [:lua5.1 :lua5.2 :lua5.3 :lua5.4 :luajit]) 48 | run #(pick-values 2 (peval $ (: "--lua %q" :format lua-exec)))] 49 | (t.= [true lua-exec] 50 | [(run (v (case (_VERSION:sub 5) 51 | :5.1 (if _G.jit :luajit :lua5.1) 52 | v-num (.. :lua v-num))))] 53 | (.. "should execute code in Lua runtime: " lua-exec)) 54 | (let [(success? output) (run (v (do (print :test) (os.exit 1 true))))] 55 | (t.= "test" output) 56 | ;; pcall in Lua 5.1 doesn't give status with (proc:close) 57 | (t.= (if (= _VERSION "Lua 5.1") true nil) 58 | success? 59 | (.. "errors should cause failing exit status with --lua " 60 | lua-exec)))))) 61 | 62 | (fn test-lua-plugin [] 63 | (let [lua-plug :test/plugin/lua-plugin.lua] 64 | (t.= [true :s] 65 | [(fennel-cli :--plugin lua-plug 66 | :-e (sh/esc (v (let [s 1] (is-in-scope s)))))] 67 | "lua plugins should be loaded with full compiler env"))) 68 | 69 | (fn test-args [] 70 | (when (and test-all? (file-exists? "fennel")) 71 | (t.= [true "-l"] [(peval "(. arg 3)" "-l")]))) 72 | 73 | {: test-cli 74 | : test-lua-flag 75 | : test-lua-plugin 76 | : test-args} 77 | -------------------------------------------------------------------------------- /test/faith.fnl: -------------------------------------------------------------------------------- 1 | ;;; faith.fnl --- The Fennel Advanced Interactive Test Helper 2 | 3 | ;; https://git.sr.ht/~technomancy/faith 4 | 5 | ;; SPDX-License-Identifier: MIT 6 | ;; SPDX-FileCopyrightText: Scott Vokes, Phil Hagelberg, and contributors 7 | 8 | ;; To use Faith, create a test runner file which calls the `run` function with 9 | ;; a list of module names. The modules should export functions whose 10 | ;; names start with `test-` and which call the assertion functions in the 11 | ;; `faith` module. 12 | 13 | ;; Copyright © 2009-2013 Scott Vokes and contributors 14 | ;; Copyright © 2023-2024 Phil Hagelberg and contributors 15 | 16 | ;; Permission is hereby granted, free of charge, to any person obtaining a copy 17 | ;; of this software and associated documentation files (the "Software"), to deal 18 | ;; in the Software without restriction, including without limitation the rights 19 | ;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | ;; copies of the Software, and to permit persons to whom the Software is 21 | ;; furnished to do so, subject to the following conditions: 22 | 23 | ;; The above copyright notice and this permission notice shall be included in 24 | ;; all copies or substantial portions of the Software. 25 | 26 | ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | ;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | ;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | ;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | ;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | ;; SOFTWARE. 33 | 34 | (local fennel (require :fennel)) 35 | 36 | ;;; helper functions 37 | 38 | (local unpack (or table.unpack _G.unpack)) 39 | 40 | (fn now [] 41 | {:real (or (and (pcall require :socket) 42 | (package.loaded.socket.gettime)) 43 | (and (pcall require :posix) 44 | (package.loaded.posix.gettimeofday) 45 | (let [t (package.loaded.posix.gettimeofday)] 46 | (+ t.sec (/ t.usec 1000000)))) 47 | nil) 48 | :approx (os.time) 49 | :cpu (os.clock)}) 50 | 51 | (fn fn? [v] (= (type v) :function)) 52 | 53 | (fn fail->string [{: where : reason : msg} name] 54 | (string.format "FAIL: %s:\n%s: %s%s\n" 55 | name where (or reason "") 56 | (or (and msg (.. " - " (tostring msg))) ""))) 57 | 58 | (fn err->string [{: msg} name] 59 | (or msg (string.format "ERROR (in %s, couldn't get traceback)" 60 | (or name "(unknown)")))) 61 | 62 | (fn get-where [start] 63 | (let [traceback (fennel.traceback nil start) 64 | (_ _ where) (traceback:find "\n\t*([^:]+:[0-9]+):")] 65 | (or where "?"))) 66 | 67 | ;;; assertions 68 | 69 | ;; while I'd prefer to remove all top-level state, this one is difficult 70 | ;; because it has to be set by every assertion, and the assertion functions 71 | ;; themselves do not have access to any stateful arguments given that they 72 | ;; are called directly from user code. 73 | (var checked 0) 74 | (var diff-cmd (or (os.getenv "FAITH_DIFF") 75 | (if (os.getenv "NO_COLOR") 76 | "diff -u %s %s" 77 | "diff -u --color=always %s %s"))) 78 | 79 | (macro wrap [flag msg ...] 80 | `(do (set ,(sym :checked) (+ ,(sym :checked) 1)) 81 | (when (not ,flag) 82 | (error {:char "F" :type :fail :tostring fail->string 83 | :reason (string.format ,...) :msg ,msg :where (get-where 4)})))) 84 | 85 | (fn pass [] {:char "." :type :pass}) 86 | 87 | (fn error-result [msg] {:char "E" :type :err :tostring err->string :msg msg}) 88 | 89 | (fn skip [] 90 | (error {:char :s :type :skip})) 91 | 92 | (fn is [got ?msg] 93 | (wrap got ?msg "Expected truthy value")) 94 | 95 | (fn error* [pat f ?msg] 96 | (case (pcall f) 97 | (true ?val) (wrap false ?msg "Expected an error, got %s" (fennel.view ?val)) 98 | (_ err) (let [err-string (if (= (type err) :string) err (fennel.view err))] 99 | (wrap (err-string:match pat) ?msg 100 | "Expected error to match pattern %s, was %s" 101 | pat err-string)))) 102 | 103 | (fn extra-fields? [t keys] 104 | (or (accumulate [extra? false k (pairs t) &until extra?] 105 | (if (= nil (. keys k)) 106 | true 107 | (tset keys k nil))) 108 | (next keys))) 109 | 110 | (fn table= [x y equal?] 111 | (let [keys {}] 112 | (and (accumulate [same? true k v (pairs x) &until (not same?)] 113 | (do (tset keys k true) 114 | (equal? v (. y k)))) 115 | (not (extra-fields? y keys))))) 116 | 117 | (fn equal? [x y] 118 | (or (= x y) 119 | (and (= (type x) :table (type y)) (table= x y equal?)))) 120 | 121 | (fn diff-report [expv gotv] 122 | (let [exp-file (os.tmpname "faithdiff1") 123 | got-file (os.tmpname "faithdiff2")] 124 | (with-open [f (io.open exp-file :w)] 125 | (f:write expv)) 126 | (with-open [f (io.open got-file :w)] 127 | (f:write gotv)) 128 | (let [diff (doto (io.popen (diff-cmd:format exp-file got-file)) 129 | (: :read) (: :read) (: :read)) ; omit header lines 130 | out (diff:read :*all)] 131 | (os.remove exp-file) 132 | (os.remove got-file) 133 | (let [(closed _ code) (diff:close)] 134 | (if (or closed (= 1 code)) 135 | (.. "\n" out) 136 | (string.format "Expected:\n%s\nGot:\n%s" expv gotv)))))) 137 | 138 | (fn =* [exp got ?msg] 139 | (let [expv (fennel.view exp) 140 | gotv (fennel.view got) 141 | report (if (and (not= expv gotv) (or (expv:find "\n") (gotv:find "\n"))) 142 | (diff-report expv gotv) 143 | (string.format "Expected %s, got %s" expv gotv))] 144 | (wrap (equal? exp got) ?msg report))) 145 | 146 | (fn not=* [exp got ?msg] 147 | (wrap (not (equal? exp got)) ?msg "Expected something other than %s" 148 | (fennel.view exp))) 149 | 150 | (fn <* [...] 151 | (let [args [...] 152 | msg (if (= :string (type (. args (length args)))) (table.remove args)) 153 | correct? (faccumulate [ok? true i 2 (length args) &until (not ok?)] 154 | (< (. args (- i 1)) (. args i)))] 155 | (wrap correct? msg 156 | "Expected arguments in strictly increasing order, got %s" 157 | (fennel.view args)))) 158 | 159 | (fn <=* [...] 160 | (let [args [...] 161 | msg (if (= :string (type (. args (length args)))) (table.remove args)) 162 | correct? (faccumulate [ok? true i 2 (length args) &until (not ok?)] 163 | (<= (. args (- i 1)) (. args i)))] 164 | (wrap correct? msg 165 | "Expected arguments in increasing/equal order, got %s" 166 | (fennel.view args)))) 167 | 168 | (fn almost= [exp got tolerance ?msg] 169 | (wrap (<= (math.abs (- exp got)) tolerance) ?msg 170 | "Expected %s +/- %s, got %s" exp tolerance got)) 171 | 172 | (fn identical [exp got ?msg] 173 | (wrap (rawequal exp got) ?msg 174 | "Expected %s, got %s" (fennel.view exp) (fennel.view got))) 175 | 176 | (fn match* [pat s ?msg] 177 | (wrap (: (tostring s) :match pat) ?msg 178 | "Expected string to match pattern %s, was\n%s" pat s)) 179 | 180 | (fn not-match [pat s ?msg] 181 | (wrap (or (not= (type s) :string) (not (s:match pat))) ?msg 182 | "Expected string not to match pattern %s, was\n %s" pat s)) 183 | 184 | ;;; running 185 | 186 | (fn dot [char total-count] 187 | (io.write char) 188 | (when (= 0 (math.fmod total-count 76)) 189 | (io.write "\n")) 190 | (io.stdout:flush)) 191 | 192 | (fn print-totals [report] 193 | (let [{: started-at : ended-at : results} report 194 | duration (fn [start end] 195 | (let [decimal-places 2] 196 | (: (.. "%." (tonumber decimal-places) "f") 197 | :format 198 | (math.max (- end start) 199 | (^ 10 (- decimal-places)))))) 200 | counts (accumulate [counts {:pass 0 :fail 0 :err 0 :skip 0} 201 | _ {:type type*} (ipairs results)] 202 | (doto counts (tset type* (+ (. counts type*) 1))))] 203 | (print (: (.. "Testing finished %s with %d assertion(s)\n" 204 | "%d passed, %d failed, %d error(s), %d skipped\n" 205 | "%.2f second(s) of CPU time used") 206 | :format 207 | (if started-at.real 208 | (: "in %s second(s)" :format 209 | (duration started-at.real ended-at.real)) 210 | (: "in approximately %s second(s)" :format 211 | (- ended-at.approx started-at.approx))) 212 | checked 213 | counts.pass 214 | counts.fail 215 | counts.err 216 | counts.skip 217 | (duration started-at.cpu ended-at.cpu))))) 218 | 219 | (fn begin-module [report tests] 220 | (print (string.format "\nStarting module %s with %d test(s)" 221 | report.module-name 222 | (accumulate [count 0 _ (pairs tests)] (+ count 1))))) 223 | 224 | (fn done [report] 225 | (print "\n") 226 | (each [_ result (ipairs report.results)] 227 | (when result.tostring (print (result:tostring result.name)))) 228 | (print-totals report)) 229 | 230 | (local default-hooks {:begin false 231 | : done 232 | : begin-module 233 | :end-module false 234 | :begin-test false 235 | :end-test (fn [_name result total-count] 236 | (dot result.char total-count))}) 237 | 238 | (fn test-key? [k] 239 | (and (= (type k) :string) (k:match :^test.*))) 240 | 241 | (local ok-types {:fail true :pass true :skip true}) 242 | 243 | (fn err-handler [name] 244 | (fn [e] 245 | (if (and (= (type e) :table) (. ok-types e.type)) 246 | e 247 | (error-result (-> (string.format "\nERROR: %s:\n%s\n" name e) 248 | (fennel.traceback 4)))))) 249 | 250 | (fn run-test [name ?setup test ?teardown report hooks context] 251 | (when (fn? hooks.begin-test) (hooks.begin-test name)) 252 | (let [result (case-try (if ?setup (xpcall ?setup (err-handler name)) true) 253 | true (xpcall #(test (unpack context)) (err-handler name)) 254 | true (pass) 255 | (catch (_ err) err))] 256 | (when ?teardown (pcall ?teardown (unpack context))) 257 | (table.insert report.results (doto result (tset :name name))) 258 | (when (fn? hooks.end-test) 259 | (hooks.end-test name result (length report.results))))) 260 | 261 | (fn run-setup-all [setup-all report module-name] 262 | (if (fn? setup-all) 263 | (case [(pcall setup-all)] 264 | [true & context] context 265 | [false err] (let [msg (: "ERROR in test module %s setup-all: %s" 266 | :format module-name err)] 267 | (table.insert report.results 268 | (doto (error-result msg) 269 | (tset :name module-name))) 270 | (values nil err))) 271 | [])) 272 | 273 | (fn run-module [hooks report module-name test-module] 274 | (assert (= :table (type test-module)) 275 | (.. "test module must be table: " module-name)) 276 | (let [module-report {: module-name :started-at (now) :results []}] 277 | (case (run-setup-all test-module.setup-all report module-name) 278 | context (do 279 | (when hooks.begin-module 280 | (hooks.begin-module module-report test-module)) 281 | (each [_ {: name : test} 282 | (ipairs (doto (icollect [name test (pairs test-module)] 283 | (if (test-key? name) 284 | {:line (. (debug.getinfo test :S) 285 | :linedefined) 286 | : name : test})) 287 | (table.sort #(< $1.line $2.line))))] 288 | (run-test name 289 | test-module.setup 290 | test 291 | test-module.teardown 292 | module-report 293 | hooks 294 | context)) 295 | (case test-module.teardown-all 296 | teardown (pcall teardown (unpack context))) 297 | (when hooks.end-module (hooks.end-module module-report)) 298 | (icollect [_ value 299 | (ipairs module-report.results) 300 | &into report.results] 301 | value))))) 302 | 303 | (fn exit [hooks] 304 | (if hooks.exit (hooks.exit 1) 305 | _G.___replLocals___ :failed 306 | (and os os.exit) (os.exit 1))) 307 | 308 | (fn run [module-names ?opts] 309 | (set (checked diff-cmd) (values 0 (or (and ?opts ?opts.diff-cmd) diff-cmd))) 310 | (io.stdout:setvbuf :line) 311 | ;; don't count load time against the test runtime 312 | (each [_ m (ipairs module-names)] (require m)) 313 | (let [hooks (setmetatable (or (?. ?opts :hooks) {}) {:__index default-hooks}) 314 | report {:module-name :main :started-at (now) :results []}] 315 | (when hooks.begin 316 | (hooks.begin report module-names)) 317 | (each [_ module-name (ipairs module-names)] 318 | (case (pcall require module-name) 319 | (true test-module) (run-module hooks report module-name test-module) 320 | (false err) (let [error (: "ERROR: Cannot load %q:\n%s" 321 | :format module-name err)] 322 | (table.insert report.results 323 | (doto (error-result error) 324 | (tset :name module-name)))))) 325 | (set report.ended-at (now)) 326 | (when hooks.done (hooks.done report)) 327 | (when (accumulate [red false 328 | _ {:type type*} (ipairs report.results) 329 | &until red] 330 | (or (= type* :fail) 331 | (= type* :err))) 332 | (exit hooks)))) 333 | 334 | (when (= ... "--tests") 335 | (run (doto [...] (table.remove 1))) 336 | (os.exit 0)) 337 | 338 | {: run : skip :version "0.2.0" 339 | : is :error error* := =* :not= not=* :< <* :<= <=* : almost= 340 | : identical :match match* : not-match} 341 | -------------------------------------------------------------------------------- /test/fuzz-string.fnl: -------------------------------------------------------------------------------- 1 | (local fennel (require :fennel)) 2 | (local faith (require :test.faith)) 3 | (local load (or _G.loadstring load)) 4 | 5 | (fn lua-string-compiler [str] 6 | "Baseline implementation we're comparing against." 7 | ((assert (load (.. "return " str))))) 8 | 9 | (fn fennel-string-compiler [str] 10 | "Our implementation. Round trip through the fennel compiler." 11 | ((assert (load (pick-values 1 (fennel.compile-string str)))))) 12 | 13 | (fn fennel-parser [str] 14 | "Our implementation. Going just through the parser" 15 | (let [[_ok loaded] [((fennel.parser str))]] 16 | loaded)) 17 | 18 | (fn into-single-line-string [str] 19 | "Gives a string that meets the following conditions: 20 | * The string is surrounded by \" marks 21 | * The string has no newlines \\n or carriage returns \\r 22 | * The string has no unescaped \" marks" 23 | (.. "\"" 24 | (-> str 25 | ;; no newlines (so that lua can parse it) 26 | (: :gsub "[\r\n]" "") 27 | ;; all quotes must be escaped (so that it doesn't break out early) 28 | (: :gsub "\\*\"" 29 | ;; make the number of slashes even 30 | #(when (= (% (length $) 2) 1) 31 | ($:sub 2)))) 32 | "\"")) 33 | 34 | (fn generate-random-string [] 35 | "generate a random single-line string to be parsed by lua or fennel" 36 | (into-single-line-string 37 | (table.concat 38 | (fcollect [_ 1 (math.random 20)] 39 | (if (< (math.random) 0.5) 40 | (let [c "{}\t'\"uxabfnrtv0123456789 " 41 | n (math.random (length c))] 42 | (c:sub n n)) 43 | (< (math.random) 0.5) 44 | (string.char (math.random 0 255)) 45 | (< (math.random) 0.5) 46 | (.. "\\u{" (if (< (math.random) 0.9) 47 | ;; valid 48 | (string.format "%x" 49 | ;; weighted toward 0 50 | ;; you have to do this with floats because lua5.1 can't handle math.random calls with 0xFFFFFFFF 51 | (math.floor (* (math.random) (math.random) 0xFFFFFFFE))) 52 | ;; invalid 53 | (table.concat 54 | (fcollect [_ 0 (math.random 10)] 55 | (string.char (math.random 0 255))))) 56 | "}") 57 | (< (math.random) 0.5) 58 | (.. "\\" (math.random 300)) 59 | (< (math.random) 0.5) 60 | " " 61 | "\\"))))) 62 | 63 | (local remove-size [8 2 1]) 64 | (fn minimize [str still-has-property?] 65 | "repeatedly find smaller and smaller `str` where (still-has-property? str)" 66 | (case (faccumulate [reduced nil 67 | i 2 (- (length str) 1) 68 | &until reduced] 69 | (accumulate [reduced nil 70 | _ num-to-remove (ipairs remove-size) 71 | &until reduced] 72 | (let [reduced (into-single-line-string 73 | (.. (str:sub 2 (- i 1)) 74 | (str:sub (+ i num-to-remove) -2)))] 75 | (if (still-has-property? reduced) reduced)))) 76 | better (minimize better still-has-property?) 77 | _ str)) 78 | 79 | (fn get-string-parse-error [string-to-parse] 80 | (let [(old-success? old-out-str) (pcall lua-string-compiler string-to-parse) 81 | (new-success? new-out-str) (pcall fennel-parser string-to-parse)] 82 | (when (or (not= old-success? new-success?) ;; one accepts string, other rejects 83 | (and old-success? (not= old-out-str new-out-str))) ;; they both accept, but with different answers 84 | (.. "discrepancy parsing string: " string-to-parse "\n" 85 | "LUA: print(fennel.view(" string-to-parse ")) -- " 86 | (if old-success? (fennel.view old-out-str) 87 | old-out-str) "\n" 88 | "FENNEL: (print (fennel.view " string-to-parse ")) ;; " 89 | (if new-success? (fennel.view new-out-str) 90 | new-out-str) "\n")))) 91 | 92 | 93 | (fn test-fuzz-string-1 [] 94 | ;; Comparing Fennel's parser to Lua's. 95 | ;; We want the same string features as Lua 5.3+/LuaJIT 96 | ;; Lua 5.2 and 5.1 don't support all the string escape codes, 97 | ;; so they're not useful as a baseline for this fuzz test. 98 | (if (or (and (= _VERSION "Lua 5.1") (not (pcall require :jit))) 99 | (= _VERSION "Lua 5.2")) 100 | (faith.skip) 101 | (let [verbose? (os.getenv "VERBOSE") 102 | seed (os.time)] 103 | (math.randomseed seed) 104 | (for [_ 1 (tonumber (or (os.getenv "FUZZ_COUNT") 256))] 105 | (let [s (generate-random-string)] 106 | (when verbose? (print s)) 107 | (when (get-string-parse-error s) 108 | (local minimized (minimize s get-string-parse-error)) 109 | (error (get-string-parse-error minimized)))))))) 110 | 111 | (fn get-string-compile-error [string-to-parse] 112 | (let [(old-success? old-out-str) (pcall fennel-string-compiler string-to-parse) 113 | (new-success? new-out-str) (pcall fennel-parser string-to-parse)] 114 | (when (or (not= old-success? new-success?) ;; one accepts string, other rejects 115 | (and old-success? (not= old-out-str new-out-str))) ;; they both accept, but with different answers 116 | (.. "discrepancy parsing string: " string-to-parse "\n" 117 | "LUA: print(fennel.view(" string-to-parse ")) -- " 118 | (if old-success? (fennel.view old-out-str) 119 | old-out-str) "\n" 120 | "FENNEL: (print (fennel.view " string-to-parse ")) ;; " 121 | (if new-success? (fennel.view new-out-str) 122 | new-out-str) "\n")))) 123 | 124 | (fn test-fuzz-string-2 [] 125 | ;; Comparing Fennel's parser to Fennel. 126 | ;; In Fennel, a string is supposed to evaluate to itself. 127 | ;; This should work, regardless of Lua version 128 | (let [verbose? (os.getenv "VERBOSE") 129 | seed (os.time)] 130 | (math.randomseed seed) 131 | (for [_ 1 (tonumber (or (os.getenv "FUZZ_COUNT") 256))] 132 | (let [s (generate-random-string)] 133 | (when verbose? (print s)) 134 | (when (get-string-compile-error s) 135 | (local minimized (minimize s get-string-compile-error)) 136 | (error (get-string-compile-error minimized))))))) 137 | 138 | 139 | {: test-fuzz-string-1 140 | : test-fuzz-string-2} 141 | -------------------------------------------------------------------------------- /test/fuzz.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | (local fennel (require :fennel)) 3 | (local generate (require :test.generate)) 4 | (local friend (require :fennel.friend)) 5 | (local unpack (or table.unpack _G.unpack)) 6 | 7 | ;; extend the generator function to produce ASTs 8 | (table.insert generate.order 4 :sym) 9 | (table.insert generate.order 1 :list) 10 | 11 | (local keywords (icollect [k (pairs (doto (fennel.syntax) 12 | (tset :eval-compiler nil) 13 | (tset :lua nil) 14 | (tset :macros nil)))] k)) 15 | 16 | (fn generate.generators.sym [] 17 | (case (: (generate.generators.string) :gsub "." 18 | (fn [c] (if (not (fennel.sym-char? c)) ""))) 19 | "" (generate.generators.sym) 20 | name (fennel.sym name))) 21 | 22 | (fn generate.generators.list [gen depth] 23 | (let [f (fennel.sym (. keywords (math.random (length keywords)))) 24 | contents (if (< 0.5 (math.random)) 25 | (generate.generators.sequence gen depth) 26 | [])] 27 | (fennel.list f (unpack contents)))) 28 | 29 | (local marker {}) 30 | 31 | (fn fuzz [verbose? seed] 32 | (let [code (fennel.view (generate.generators.list generate.generate 1)) 33 | (ok err) (xpcall #(fennel.compile-string code {:useMetadata true 34 | :compiler-env :strict}) 35 | #(if (= $ marker) 36 | marker 37 | (.. (tostring $) "\n" (debug.traceback))))] 38 | (when verbose? 39 | (print code)) 40 | (if (not ok) 41 | ;; if we get an error, it must come from assert-compile; if we get 42 | ;; a non-assertion error then it must be a compiler bug! 43 | (t.= err marker (.. code "\n" (tostring err) "\nSeed: " seed)) 44 | (let [(ok2 err2) ((or _G.loadstring load) err)] 45 | ;; if we get an err2, it must mean that fennel's output isn't valid Lua 46 | ;; If fennel emits code, it should be valid Lua! 47 | (when (not ok2) 48 | (error (.. (tostring err2) "\n" code "\n" (tostring err) "\nSeed: " seed))))))) 49 | 50 | (fn test-fuzz [] 51 | (let [verbose? (os.getenv "VERBOSE") 52 | {: assert-compile : parse-error} friend 53 | seed (os.time)] 54 | (math.randomseed seed) 55 | (set friend.assert-compile #(error marker)) 56 | (set friend.parse-error #(error marker)) 57 | (for [_ 1 (tonumber (or (os.getenv "FUZZ_COUNT") 256))] 58 | (fuzz verbose? seed)) 59 | (set friend.assert-compile assert-compile) 60 | (set friend.parse-error parse-error))) 61 | 62 | {: test-fuzz} 63 | -------------------------------------------------------------------------------- /test/generate.fnl: -------------------------------------------------------------------------------- 1 | ;; A general-purpose function for generating random values. 2 | 3 | (local random-char 4 | (fn [] 5 | (if (> (math.random) 0.9) ; digits 6 | (string.char (+ 47 (math.random 10))) 7 | (> (math.random) 0.5) ; lower case 8 | (string.char (+ 96 (math.random 26))) 9 | (> (math.random) 0.5) ; upper case 10 | (string.char (+ 64 (math.random 26))) 11 | (> (math.random) 0.5) ; space and punctuation 12 | (string.char (+ 31 (math.random 16))) 13 | (> (math.random) 0.5) ; newlines and tabs 14 | (string.char (+ 9 (math.random 4))) 15 | :else ; bonus punctuation 16 | (string.char (+ 58 (math.random 5)))))) 17 | 18 | (local generators {:number (fn [] ; weighted towards mid-range integers 19 | (if (> (math.random) 0.9) 20 | (let [x (math.random 2147483647)] 21 | (math.floor (- x (/ x 2)))) 22 | (> (math.random) 0.2) 23 | (math.floor (math.random 2048)) 24 | :else (math.random))) 25 | :string (fn [] 26 | (var s "") 27 | (for [_ 1 (math.random 16)] 28 | (set s (.. s (random-char)))) 29 | s) 30 | :table (fn [generate depth] 31 | (let [t {}] 32 | (var k nil) 33 | (for [_ 1 (math.random 16)] 34 | (set k (generate depth)) 35 | ;; no nans plz 36 | (while (not= k k) (set k (generate depth))) 37 | (when (not= nil k) 38 | (tset t k (generate depth)))) 39 | t)) 40 | :sequence (fn [generate depth] 41 | (let [t {}] 42 | (for [_ 1 (math.random 32)] 43 | (tset t (+ (length t) 1) (generate depth))) 44 | t)) 45 | :boolean (fn [] (> (math.random) 0.5)) 46 | :list (fn [] [])}) 47 | 48 | (local order [:number :string :table :sequence :boolean]) 49 | 50 | (fn generate [depth ?choice] 51 | "Generate a random piece of data." 52 | (if (< (+ 0.5 (/ (math.log depth 10) 1.2)) (math.random)) 53 | (match (. generators (or (. order (or ?choice 1)) :boolean)) 54 | generator (generator generate (+ depth 1))) 55 | (or (= nil ?choice) (<= ?choice (length order))) 56 | (generate depth (+ (or ?choice 1) 1)))) 57 | 58 | {: generate : generators : order} 59 | -------------------------------------------------------------------------------- /test/indirect-macro.fnl: -------------------------------------------------------------------------------- 1 | (local m (require :test.macros)) 2 | 3 | {:inc2 (fn [...] (m.inc ...))} 4 | -------------------------------------------------------------------------------- /test/init.lua: -------------------------------------------------------------------------------- 1 | local t = require("test.faith") 2 | 3 | -- Ensure we don't accidentally set globals when loading or running the compiler 4 | setmetatable(_G, {__newindex=function(_, k) error("set global "..k) end}) 5 | 6 | local oldfennel = require("bootstrap.fennel") 7 | local opts = {useMetadata = true, correlate = true} 8 | oldfennel.dofile("src/fennel.fnl").install(opts) 9 | 10 | local modules = {"test.core", "test.mangling", "test.quoting", "test.bit", 11 | "test.fennelview", "test.parser", "test.failures", "test.repl", 12 | "test.cli", "test.macro", "test.linter", "test.loops", 13 | "test.misc", "test.searcher", "test.api", "test.sourcemap"} 14 | 15 | if(#arg ~= 0 and arg[1] ~= "--eval") then modules = arg end 16 | 17 | t.run(modules,{hooks={exit=dofile("test/irc.lua")}}) 18 | -------------------------------------------------------------------------------- /test/irc.lua: -------------------------------------------------------------------------------- 1 | local server_port = (os.getenv("IRC_HOST_PORT") or "irc.libera.chat 6667") 2 | local channel = os.getenv("IRC_CHANNEL") 3 | local url = os.getenv("JOB_URL") or "???" 4 | 5 | local remote = io.popen("git remote get-url origin 2> /dev/null"):read('*l') 6 | if remote == nil then 7 | -- no git / no git repo, this is not an upstream CI job 8 | return function() end 9 | end 10 | local is_origin = remote:find('~technomancy/fennel$') ~= nil 11 | 12 | local branch = io.popen("git rev-parse --abbrev-ref HEAD"):read('*l') 13 | local is_main = branch == 'main' 14 | 15 | -- This may fail in future if libera chat once again blocks builds.sr.ht 16 | -- from connecting; it currently works after we asked them to look into it 17 | return function(failure_count) 18 | if (0 ~= tonumber(failure_count)) and is_main and is_origin and channel then 19 | print("Announcing failure on", server_port, channel) 20 | 21 | local git_log = io.popen("git log --oneline -n 1 HEAD") 22 | local log = git_log:read("*a"):gsub("\n", " "):gsub("\n", " ") 23 | 24 | local nc = io.popen(string.format("nc %s > /dev/null", server_port), "w") 25 | 26 | nc:write("NICK fennel-build\n") 27 | nc:write("USER fennel-build 8 x : fennel-build\n") 28 | nc:write("JOIN " .. channel .. "\n") 29 | nc:write(string.format("PRIVMSG %s :Build failure! %s / %s\n", 30 | channel, log, url)) 31 | nc:write("QUIT\n") 32 | nc:close() 33 | end 34 | if(failure_count ~= 0) then os.exit(1) end 35 | end 36 | -------------------------------------------------------------------------------- /test/linter.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | (local fennel (require :fennel)) 3 | (local linter (fennel.dofile "src/linter.fnl" {:env :_COMPILER :compilerEnv _G})) 4 | (local options {:plugins [linter]}) 5 | 6 | (fn test-used [] 7 | "A test for the locals shadowing bug described in 8 | https://todo.sr.ht/~technomancy/fennel/12" 9 | (let [src "(fn [abc] (let [abc abc] abc))" 10 | (ok? msg) (pcall fennel.compile-string src options)] 11 | (t.is ok? msg))) 12 | 13 | (fn test-arity-check [] 14 | (let [src "(let [s (require :test.mod.splice)] (s.myfn 1))" 15 | ok? (pcall fennel.compile-string src options)] 16 | (when (not= _VERSION "Lua 5.1") ; debug.getinfo nparams was added in 5.2 17 | (t.is (not ok?))))) 18 | 19 | (fn test-missing-fn [] 20 | (let [src "(let [s (require :test.mod.splice)] (s.missing-fn))" 21 | ok? (pcall fennel.compile-string src options)] 22 | (t.is (not ok?)))) 23 | 24 | (fn test-var-never-set [] 25 | (t.is (not (pcall fennel.compile-string "(var x 1) (+ x 9)" options))) 26 | (t.is (pcall fennel.compile-string "(var x 1) (set x 9)" options))) 27 | 28 | (fn teardown [] 29 | (let [utils (require :fennel.utils)] 30 | (set utils.root.options.plugins {}))) 31 | 32 | {: test-used 33 | : test-arity-check 34 | : test-missing-fn 35 | : test-var-never-set 36 | : teardown} 37 | -------------------------------------------------------------------------------- /test/loops.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | (local fennel (require :fennel)) 3 | 4 | (macro == [form expected ?opts] 5 | `(let [(ok# val#) (pcall fennel.eval ,(view form) ,?opts)] 6 | (t.is ok# val#) 7 | (t.= val# ,expected))) 8 | 9 | (fn test-each [] 10 | (== (each [x (pairs [])] nil) nil) 11 | (== (let [t {:a 1 :b 2} t2 {}] 12 | (each [k v (pairs t)] 13 | (tset t2 k v)) 14 | (+ t2.a t2.b)) 3) 15 | (== (do (var t 0) (local (f s v) (pairs [1 2 3])) 16 | (each [_ x (values f (doto s (table.remove 1)))] (set t (+ t x))) t) 5) 17 | (== (do (var t 0) (local (f s v) (pairs [1 2 3])) 18 | (each [_ x (values f s v)] (set t (+ t x))) t) 6) 19 | (== (do (var x 0) (while (< x 7) (set x (+ x 1))) x) 7) 20 | (== (let [x [] y [1 2 3]] 21 | (each [(i n) (ipairs y)] 22 | (table.insert x (+ i n))) 23 | x) [2 4 6])) 24 | 25 | (fn test-for [] 26 | (== (for [y 0 2] nil) nil) 27 | (== (do (var x 0) (for [y 1 20 2] (set x (+ x 1))) x) 10) 28 | (== (do (var x 0) (for [y 1 5] (set x (+ x 1))) x) 5)) 29 | 30 | (fn test-comprehensions [] 31 | (== (collect [k v (pairs {:a 1 :b 2 :c 3})] v k) 32 | [:a :b :c]) 33 | (== (collect [k v (pairs {:apple :red :orange :orange})] 34 | (values (.. :color- v) (.. :fruit- k))) 35 | {:color-red :fruit-apple :color-orange :fruit-orange}) 36 | (== (collect [k v (pairs {:foo 3 :bar 4 :baz 5 :qux 6})] 37 | (if (> v 4) (values k (+ v 1)))) 38 | {:baz 6 :qux 7}) 39 | (== (collect [k v (pairs {:neon :lights}) &into {:shimmering-neon :lights}] 40 | (values k (v:upper))) 41 | {:neon "LIGHTS" :shimmering-neon "lights"}) 42 | (== (icollect [_ v (ipairs [1 2 3 4 5 6])] 43 | (if (= 0 (% v 2)) (* v v))) 44 | [4 16 36]) 45 | (== (icollect [num (string.gmatch "24,58,1999" "%d+")] 46 | (tonumber num)) 47 | [24 58 1999]) 48 | (== (icollect [_ x (ipairs [2 3]) &into [11]] (* x 11)) 49 | [11 22 33]) 50 | (== (let [xs [11]] (icollect [_ x (ipairs [2 3]) &into xs] (* x 11))) 51 | [11 22 33]) 52 | (let [code "(icollect [_ x (ipairs [2 3]) &into \"oops\"] x)" 53 | (ok? msg) (pcall fennel.compileString code)] 54 | (t.is (not ok?)) 55 | (t.match "&into clause" msg)) 56 | (let [code "(icollect [_ x (ipairs [2 3]) &into 2] x)" 57 | (ok? msg) (pcall fennel.compileString code)] 58 | (t.is (not ok?)) 59 | (t.match "&into clause" msg)) 60 | (== (do (macro twice [expr] `(do ,expr ,expr)) 61 | (twice (icollect [i v (ipairs [:a :b :c])] v))) 62 | [:a :b :c]) 63 | (== (let [result [0]] 64 | (icollect [_ v (ipairs [1 2 [3 4 5] 6 7]) &into result] 65 | (case (type v) 66 | :table (do 67 | (icollect [_ e (ipairs v) &into result] e) 68 | nil) 69 | _ v))) 70 | [0 1 2 3 4 5 6 7]) 71 | (== (fcollect [i 1 4] i) 72 | [1 2 3 4]) 73 | (== (fcollect [i 1 4 2] i) 74 | [1 3]) 75 | (== (fcollect [i 1 10 2 &until (> i 5)] i) 76 | [1 3 5]) 77 | (== (fcollect [i 1 10 2 :until (> i 5)] i) 78 | [1 3 5]) 79 | (== (fcollect [i 1 4 &into [0]] i) 80 | [0 1 2 3 4]) 81 | (== (fcollect [i 1 4 :into [0]] i) 82 | [0 1 2 3 4]) 83 | (== (fcollect [i 1 4 2 &into [0]] i) 84 | [0 1 3]) 85 | (== (fcollect [i 1 10 2 86 | &into [0] 87 | &until (> i 5)] 88 | (when (not= i 3) i)) 89 | [0 1 5])) 90 | 91 | (fn test-accumulate [] 92 | (== (do (var x true) 93 | (let [y (accumulate [state :init 94 | _ _ (pairs {})] 95 | (do (set x false) 96 | :update))] 97 | [x y])) 98 | [true :init]) 99 | (== (accumulate [s :fen 100 | _ c (ipairs [:n :e :l :o]) &until (>= c :o)] 101 | (.. s c)) 102 | "fennel") 103 | (== (accumulate [n 0 _ _ (pairs {:one 1 :two nil :three 3})] 104 | (+ n 1)) 105 | 2) 106 | (== (accumulate [yes? true 107 | _ s (ipairs [:yes :no :yes])] 108 | (and yes? (string.match s :yes))) 109 | nil) 110 | (== (let [(a b) (accumulate [(x y) (values 8 2) _ (ipairs [1])] (values y x))] 111 | (+ a b)) 10) 112 | (== (do (macro twice [expr] `(do ,expr ,expr)) 113 | (twice (accumulate [s "" _ v (ipairs [:a :b])] (.. s v)))) 114 | :ab)) 115 | 116 | (fn test-faccumulate [] 117 | (== 15 (faccumulate [sum 0 i 1 5] (+ sum i))) 118 | (== 6 (faccumulate [sum 0 i 1 5 &until (= i 4)] (+ sum i))) 119 | (== "EDCBA" (faccumulate [alphabet "" i 4 0 -1] 120 | (.. alphabet (string.char (+ i 65)))))) 121 | 122 | (fn test-conditions [] 123 | (== (do (var x 0) (for [i 1 10 &until (= i 5)] (set x i)) x) 4) 124 | (== (do (var x 0) (each [_ i (ipairs [1 2 3]) &until (< 2 x)] (set x i)) x) 3) 125 | (== (icollect [_ i (ipairs [4 5 6]) &until (= i 5)] i) [4]) 126 | (== (collect [i x (pairs [4 5 6]) &until (= x 6)] (values i x)) [4 5]) 127 | (== (icollect [i x (pairs [4 5 6]) &into [3] &until (= x 6)] x) [3 4 5])) 128 | 129 | {: test-each 130 | : test-for 131 | : test-comprehensions 132 | : test-accumulate 133 | : test-faccumulate 134 | : test-conditions} 135 | -------------------------------------------------------------------------------- /test/luabad.lua: -------------------------------------------------------------------------------- 1 | return {bad=function() assert(_G["os"]) return "bad" end} 2 | -------------------------------------------------------------------------------- /test/luamod.lua: -------------------------------------------------------------------------------- 1 | return {abc=function() return "abc" end} 2 | -------------------------------------------------------------------------------- /test/macros.fnl: -------------------------------------------------------------------------------- 1 | ;; this module is loaded by the test suite. 2 | 3 | (fn def [] (error "oh no") 32) 4 | (fn abc [] (def) 1) 5 | 6 | {"->1" (fn [val ...] 7 | (var x val) 8 | (each [_ elt (ipairs [...])] 9 | (table.insert elt 2 x) 10 | (set x elt)) 11 | x) 12 | :defn1 (fn [name args ...] 13 | (assert (sym? name) "defn1: function names must be symbols") 14 | `(global ,name (fn ,args ,...))) 15 | :inc (fn [n] "Increments n by 1" 16 | (if (not (list? n)) `(+ ,n 1) 17 | `(let [num# ,n] (+ num# 1)))) 18 | :inc! (fn [a ?n] `(set ,a (+ ,a ,(or ?n 1)))) 19 | :multigensym (fn [] 20 | `(let [x# {:abc (fn [] 518)} 21 | y# {:one 1}] 22 | (+ (x#:abc) y#.one))) 23 | :unsandboxed (fn [] (view [:no :sandbox])) 24 | :fail-one (fn [x] (when (= x 1) (abc)) true) 25 | :gensym-shadow (fn [] 26 | (let [g (gensym)] 27 | `(let [,g (let [,g 1] 2)] 28 | ,g)))} 29 | -------------------------------------------------------------------------------- /test/mangling.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | (local fennel (require :fennel)) 3 | 4 | (local mangling-tests {:3 "__fnl_global__3" 5 | :a "a" 6 | :a-b-c "__fnl_global__a_2db_2dc" 7 | :a_3 "a_3" 8 | :a_b-c "__fnl_global__a_5fb_2dc" 9 | :break "__fnl_global__break"}) 10 | 11 | (fn test-mangling [] 12 | (each [k v (pairs mangling-tests)] 13 | (let [manglek (fennel.mangle k) 14 | unmanglev (fennel.unmangle v)] 15 | (t.= v manglek) 16 | (t.= k unmanglev))) 17 | ;; adding an env for evaluation causes global mangling rules to apply 18 | (t.is (fennel.eval "(global mangled-name true) mangled-name" 19 | {:env {}}))) 20 | 21 | (fn test-keyword-mangling [] 22 | (let [code "(local new 99)" 23 | opts {:keywords {"new" true}}] 24 | (t.match "local _new = 99" (fennel.compile-string code opts)))) 25 | 26 | {: test-mangling 27 | : test-keyword-mangling} 28 | -------------------------------------------------------------------------------- /test/misc.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | (local fennel (require :fennel)) 3 | (local view (require :fennel.view)) 4 | 5 | (fn test-leak [] 6 | (t.is (not (pcall fennel.eval "(->1 1 (+ 4))" {:allowedGlobals false})) 7 | "Expected require-macros not leak into next evaluation.")) 8 | 9 | (fn test-runtime-quote [] 10 | (t.is (not (pcall fennel.eval "`(hey)" {:allowedGlobals false})) 11 | "Expected quoting lists to fail at runtime.") 12 | (t.is (not (pcall fennel.eval "`[hey]" {:allowedGlobals false})) 13 | "Expected quoting syms to fail at runtime.")) 14 | 15 | (fn test-global-mangling [] 16 | (t.is (pcall fennel.eval "(.. hello-world :w)" {:env {:hello-world "hi"}}) 17 | "Expected global mangling to work.")) 18 | 19 | (fn test-include [] 20 | (tset package.preload :test.mod.quux nil) 21 | (let [stderr io.stderr 22 | ;; disable warnings because these are supposed to fall back 23 | _ (set io.stderr nil) 24 | expected "foo:FOO-1bar:BAR-2-BAZ-3" 25 | (ok out) (pcall fennel.dofile "test/mod/foo.fnl") 26 | (ok2 out2) (pcall fennel.dofile "test/mod/foo2.fnl" 27 | {:requireAsInclude true}) 28 | (ok3 out3) (pcall fennel.dofile "test/mod/foo3.fnl" 29 | {:requireAsInclude true}) 30 | (ok4 out4) (pcall fennel.dofile "test/mod/foo4.fnl") 31 | (ok5 out5) (pcall fennel.dofile "test/mod/foo5.fnl" 32 | {:requireAsInclude true} 33 | :test) 34 | (ok6 out6) (pcall fennel.dofile "test/mod/foo6.fnl" 35 | {:requireAsInclude true} 36 | :test) 37 | (ok6-2 out6-2) (pcall fennel.dofile "test/mod/foo6-2.fnl" 38 | {:requireAsInclude true} 39 | :test)] 40 | (t.is ok (: "Expected foo to run but it failed with error %s" :format (tostring out))) 41 | (t.is ok2 (: "Expected foo2 to run but it failed with error %s" :format (tostring out2))) 42 | (t.is ok3 (: "Expected foo3 to run but it failed with error %s" :format (tostring out3))) 43 | (t.is ok4 (: "Expected foo4 to run but it failed with error %s" :format (tostring out4))) 44 | (t.is ok5 (: "Expected foo5 to run but it failed with error %s" :format (tostring out5))) 45 | (t.is ok6 (: "Expected foo6 to run but it failed with error %s" :format (tostring out6))) 46 | (t.is ok6-2 (: "Expected foo6 to run but it failed with error %s" :format (tostring out6-2))) 47 | (t.= expected (and (= :table (type out)) out.result) 48 | (.. "Expected include to have result: " expected)) 49 | (t.= [:FOO 1] out.quux 50 | "Expected include to expose upvalues into included modules") 51 | (t.= (view out) (view out2) 52 | "Expected requireAsInclude to behave the same as include") 53 | (t.= (view out) (view out3) 54 | "Expected requireAsInclude to behave the same as include when given an expression") 55 | (t.= (view out) (view out4) 56 | "Expected include to work when given an expression") 57 | (t.= (view out) (view out5) 58 | "Expected relative requireAsInclude to work when given a ...") 59 | (t.= (view out) (view out6) 60 | "Expected relative requireAsInclude to work with nested modules") 61 | (t.= (view out) (view out6-2) 62 | "Expected relative requireAsInclude to work with nested modules") 63 | (t.= nil _G.quux "Expected include to actually be local") 64 | (let [spliceOk (pcall fennel.dofile "test/mod/splice.fnl")] 65 | (t.is spliceOk "Expected splice to run") 66 | (t.= nil _G.q "Expected include to actually be local")) 67 | (set io.stderr stderr)) 68 | (let [stderr io.stderr 69 | stderr-fail false 70 | _ (set io.stderr {:write #(do (set-forcibly! stderr-fail $2) nil)}) 71 | code "(local (bar-ok bar) (pcall #(require :test.mod.bar))) 72 | (local baz (require :test.mod.baz)) 73 | (local (quux-ok quux) (pcall #(require :test.mod.quuuuuuux))) 74 | [(when bar-ok bar) baz (when quux-ok quux)]" 75 | opts {:requireAsInclude true :skipInclude [:test.mod.bar :test.mod.quuuuuuux]} 76 | out (fennel.compile-string code opts) 77 | value (fennel.eval code opts)] 78 | (t.match "baz = require%(\"test.mod.baz\"%)" out) 79 | (t.match "bar = pcall" out) 80 | (t.match "quux = pcall" out) 81 | (t.not-match "baz = nil" out) 82 | (t.= stderr-fail false) 83 | (t.= value [[:BAR 2 :BAZ 3] [:BAZ 3] nil]) 84 | (set io.stderr stderr))) 85 | 86 | (fn test-env-iteration [] 87 | (let [tbl [] 88 | g {:hello-world "hi" 89 | :pairs (fn [t] (local mt (getmetatable t)) 90 | (if (and mt mt.__pairs) 91 | (mt.__pairs t) 92 | (pairs t))) 93 | :tbl tbl} 94 | e []] 95 | (set g._G g) 96 | (fennel.eval "(each [k (pairs _G)] (tset tbl k true))" {:env g}) 97 | (t.is (. tbl "hello-world") 98 | "Expected wrapped _G to support env iteration.") 99 | (var k []) 100 | (fennel.eval "(global x-x 42)" {:env e}) 101 | (fennel.eval "x-x" {:env e}) 102 | (each [mangled (pairs e)] 103 | (set k mangled)) 104 | (t.= (. e k) 42 105 | "Expected mangled globals to be kept across eval invocations."))) 106 | 107 | (fn test-empty-values [] 108 | (t.is (fennel.eval 109 | "(let [a (values) 110 | b (values (values)) 111 | (c d) (values) 112 | e (if (values) (values)) 113 | f (while (values) (values)) 114 | [g] [(values)] 115 | {: h} {:h (values)}] 116 | (not (or a b c d e f g h)))") 117 | "empty (values) should resolve to nil") 118 | (t.= (fennel.eval "(select :# (values))") 0) 119 | (t.= (fennel.eval "(select :# (#(values)))") 0) 120 | (let [broken-code (fennel.compile "(local [x] (values)) (local {: y} (values))")] 121 | (t.is broken-code "code should compile") 122 | (t.error "attempt to call a string" broken-code "should fail at runtime"))) 123 | 124 | (fn test-short-circuit [] 125 | (let [method-code "(var shorted? false) 126 | (fn set-shorted! [] (set shorted? true) {:f! (fn [])}) 127 | (and false (: (set-shorted!) :f!)) 128 | shorted?" 129 | comparator-code "(and false (< 1 (error :nein!) 3))"] 130 | (t.is (not (fennel.eval method-code))) 131 | (t.is (not (fennel.eval comparator-code))))) 132 | 133 | (fn test-precedence [] 134 | (let [bomb (setmetatable {} {:__add #(= $2 false)})] 135 | (t.is (fennel.eval "(+ x (<= 1 5 3))" {:env {:x bomb : _G}}) 136 | "n-ary comparators should ignore operators precedence"))) 137 | 138 | (fn test-table [] 139 | (let [code "{:transparent 0 :sky 0 :sun 1 :stem 2 :cloud 3 :star 3 :moon 3 140 | :cloud-2 4 :gray 4 :rain 5 :butterfly-body 6 :bee-body-1 6 :white 3 141 | :butterfly-eye 7 :bee-body-2 7 :dying-plant 7 8 8 9 9}" 142 | tbl (fennel.eval code)] 143 | (t.= (. tbl 8) 8))) 144 | 145 | (fn test-multisyms [] 146 | (t.is (pcall fennel.eval "(let [x {:0 #$1 :& #$1}] (x:0) (x:&) (x.0) (x.&))" {:allowedGlobals false}) 147 | "Expected to be able to use multisyms with digits and & in their second part")) 148 | 149 | (fn test-strings [] 150 | ;; need bs var in order to test effectively while testing on escape issues 151 | ;; that may be broken in the self-hosetd fennel verfsion 152 | (let [bs (string.char 92)] ; backslash 153 | (macro compile-string [...] 154 | `(-> (fennel.compile-string ,...) 155 | (: :gsub "^return " "") 156 | (: :gsub "^\"([^\"]+)\"$" "%1"))) 157 | (t.= "\\r\\n" (compile-string "\"\r\n\"") 158 | "expected compiling newlines to preserve backslash") 159 | (t.= (.. bs "127") (compile-string (.. "\"" bs "127\"")) 160 | (.. "expected " bs " to output the byte for 3-digit escapes")) 161 | (t.= (.. bs bs "12") (compile-string (.. "\"" bs bs "12\"")) 162 | (.. "expected even # of " bs "'s not to escape what follows")))) 163 | 164 | {: test-empty-values 165 | : test-env-iteration 166 | : test-global-mangling 167 | : test-include 168 | : test-leak 169 | : test-table 170 | : test-runtime-quote 171 | : test-short-circuit 172 | : test-precedence 173 | : test-multisyms 174 | : test-strings} 175 | -------------------------------------------------------------------------------- /test/mod/bar.fnl: -------------------------------------------------------------------------------- 1 | (local bar [:BAR 2]) 2 | (each [_ v (ipairs (include :test.mod.baz))] 3 | (table.insert bar v)) 4 | bar 5 | -------------------------------------------------------------------------------- /test/mod/baz.fnl: -------------------------------------------------------------------------------- 1 | [:BAZ 3] 2 | -------------------------------------------------------------------------------- /test/mod/foo.fnl: -------------------------------------------------------------------------------- 1 | (local foo [:FOO 1]) 2 | (local quux (include :test.mod.quux)) 3 | (local bar (include :test.mod.bar)) 4 | {:result (.. "foo:" (table.concat foo "-") "bar:" (table.concat bar "-")) 5 | : quux} 6 | -------------------------------------------------------------------------------- /test/mod/foo2.fnl: -------------------------------------------------------------------------------- 1 | (local foo [:FOO 1]) 2 | (local quux (require :test.mod.quux)) 3 | (local bar (require :test.mod.bar)) 4 | {:result (.. "foo:" (table.concat foo "-") "bar:" (table.concat bar "-")) 5 | : quux} 6 | -------------------------------------------------------------------------------- /test/mod/foo3.fnl: -------------------------------------------------------------------------------- 1 | (local foo [:FOO 1]) 2 | (local quux (require (.. :test :.mod.quux))) 3 | (local bar (require (.. :test :.mod :.bar))) 4 | {:result (.. "foo:" (table.concat foo "-") "bar:" (table.concat bar "-")) 5 | : quux} 6 | -------------------------------------------------------------------------------- /test/mod/foo4.fnl: -------------------------------------------------------------------------------- 1 | (local foo [:FOO 1]) 2 | (local quux (include (.. :test :.mod.quux))) 3 | (local bar (include (.. :test :.mod :.bar))) 4 | {:result (.. "foo:" (table.concat foo "-") "bar:" (table.concat bar "-")) 5 | : quux} 6 | -------------------------------------------------------------------------------- /test/mod/foo5.fnl: -------------------------------------------------------------------------------- 1 | (local foo [:FOO 1]) 2 | (local quux (require (.. ... :.mod.quux))) 3 | (local bar (require (.. ... :.mod :.bar))) 4 | {:result (.. "foo:" (table.concat foo "-") "bar:" (table.concat bar "-")) 5 | : quux} 6 | -------------------------------------------------------------------------------- /test/mod/foo6-2.fnl: -------------------------------------------------------------------------------- 1 | (local foo [:FOO 1]) 2 | (local quux (require (.. ... :.mod.quux))) 3 | (local bar (require (.. ... :.mod.nested-2.mod1))) 4 | {:result (.. "foo:" (table.concat foo "-") "bar:" (table.concat bar "-")) 5 | : quux} 6 | -------------------------------------------------------------------------------- /test/mod/foo6.fnl: -------------------------------------------------------------------------------- 1 | (local foo [:FOO 1]) 2 | (local quux (require (.. ... :.mod.quux))) 3 | (local bar (require (.. ... :.mod.nested.mod1))) 4 | {:result (.. "foo:" (table.concat foo "-") "bar:" (table.concat bar "-")) 5 | : quux} 6 | -------------------------------------------------------------------------------- /test/mod/foo7.fnl: -------------------------------------------------------------------------------- 1 | (fn foo [] :foo) 2 | 3 | {: foo} 4 | -------------------------------------------------------------------------------- /test/mod/macroed.fnlm: -------------------------------------------------------------------------------- 1 | {:reverse3 (fn [[a b c]] [c b a])} 2 | -------------------------------------------------------------------------------- /test/mod/nested-2/mod1.fnl: -------------------------------------------------------------------------------- 1 | (require (.. (: (or ... "") :match "(.+)%.[^.]+") :.mod2)) 2 | -------------------------------------------------------------------------------- /test/mod/nested-2/mod2.fnl: -------------------------------------------------------------------------------- 1 | (local bar [:BAR 2]) 2 | (each [_ v (ipairs (include :test.mod.baz))] 3 | (table.insert bar v)) 4 | bar 5 | -------------------------------------------------------------------------------- /test/mod/nested/mod1.fnl: -------------------------------------------------------------------------------- 1 | (require (: (or ... "") :gsub "(nested%.).*$" "%1mod2")) 2 | -------------------------------------------------------------------------------- /test/mod/nested/mod2.fnl: -------------------------------------------------------------------------------- 1 | (local bar [:BAR 2]) 2 | (each [_ v (ipairs (include :test.mod.baz))] 3 | (table.insert bar v)) 4 | bar 5 | -------------------------------------------------------------------------------- /test/mod/quux.lua: -------------------------------------------------------------------------------- 1 | return foo or false 2 | -------------------------------------------------------------------------------- /test/mod/reverse.lua: -------------------------------------------------------------------------------- 1 | local function reverse(ast) 2 | local l = list() 3 | for _,x in ipairs(ast) do 4 | table.insert(l, 1, x) 5 | end 6 | return l 7 | end 8 | 9 | return {reverse=reverse} 10 | -------------------------------------------------------------------------------- /test/mod/splice.fnl: -------------------------------------------------------------------------------- 1 | (when true (table.concat [] :yes)) 2 | (local q (include "test.mod.quux")) 3 | 4 | (setmetatable {:myfn (fn [a b c] (print a b c))} 5 | {:arity-check? true}) 6 | -------------------------------------------------------------------------------- /test/mod/tracer.fnl: -------------------------------------------------------------------------------- 1 | (local fennel (require :fennel)) 2 | 3 | (fn inner [] 4 | (let [t (fennel.traceback)] 5 | nil ; don't put traceback in tail call 6 | t)) 7 | 8 | (fn outer [_arg] 9 | (let [t (inner)] 10 | nil 11 | t)) 12 | 13 | (fn info [] 14 | (let [data (fennel.getinfo 1 :LS)] 15 | ;; don't TCO me bro 16 | data)) 17 | 18 | (fn nest [] 19 | (if (= 2 (+ 1 1)) 20 | (if :hey 21 | (table.concat [])))) 22 | 23 | (fn coro [] 24 | (coroutine.yield) 25 | (print :haha)) 26 | 27 | {: outer : info : nest : coro} 28 | -------------------------------------------------------------------------------- /test/other-macros/init-macros.fnl: -------------------------------------------------------------------------------- 1 | {:m #"testing macro path"} 2 | -------------------------------------------------------------------------------- /test/parser.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | (local fennel (require :fennel)) 3 | (local utils (require :fennel.utils)) 4 | 5 | (fn == [a b msg] 6 | (t.= (fennel.view a) (fennel.view b) msg)) 7 | 8 | (fn parse= [expected str msg] 9 | (let [(ok? parsed) ((fennel.parser str))] 10 | (t.is ok?) 11 | (t.= expected parsed msg))) 12 | 13 | (fn test-basics [] 14 | (t.= "\\" (fennel.eval "\"\\\\\"")) 15 | (t.= "abc\n\240" (fennel.eval "\"abc\n\\240\"")) 16 | (t.= "abc\"def" (fennel.eval "\"abc\\\"def\"")) 17 | (t.= "abc\240" (fennel.eval "\"abc\\240\"")) 18 | (t.= 150000 (fennel.eval "150_000")) 19 | (t.= "\n5.2" (fennel.eval "\"\n5.2\"")) 20 | (t.= "zero" (fennel.eval "(let [_0 :zero] _0)") 21 | "leading underscore should be symbol, not number") 22 | (t.= "foo\nbar" (fennel.eval "\"foo\\\nbar\"") 23 | "backslash+newline should be just a newline like Lua") 24 | (t.= [true (fennel.sym "&abc")] 25 | [((fennel.parser (fennel.string-stream "&abc ")))])) 26 | 27 | (fn test-spicy-numbers [] 28 | (t.= "141791343654238" 29 | (fennel.view (fennel.eval "141791343654238"))) 30 | (t.= "141791343654238" 31 | (fennel.view (fennel.eval "1.41791343654238e+14"))) 32 | (t.= "14179134365.125" 33 | (fennel.view (fennel.eval "14179134365.125"))) 34 | (t.match "1%.41791343654238e%+0?15" 35 | (fennel.view (fennel.eval "1.41791343654238e+15"))) 36 | (t.match "2%.3456789012e%+0?76" 37 | (fennel.view (fennel.eval (.. "23456789012" (string.rep "0" 66))))) 38 | (t.match "1%.23456789e%-0?13" 39 | (fennel.view (fennel.eval "1.23456789e-13"))) 40 | (t.= ".inf" (fennel.view (fennel.eval "1e+999999"))) 41 | (t.= "-.inf" (fennel.view (fennel.eval "-1e+999999"))) 42 | (t.= "nan" (tostring (fennel.eval ".nan"))) 43 | ;; ensure we consistently treat nan as symbol even on 5.1 44 | (t.= :not-really (fennel.eval "(let [nan :not-really] nan)")) 45 | (t.= :nah (fennel.eval "(let [-nan :nah] -nan)"))) 46 | 47 | (fn test-escapes [] 48 | (parse= " " "\"\\032\"") 49 | (parse= " " "\"\\x20\"") 50 | (parse= " " "\"\\u{20}\"") 51 | (parse= "\t\n\v" "\"\\t\\n\\v\"") 52 | ;; extra unicode cases 53 | (parse= "\x24" "\"\\u{24}\"") 54 | (parse= "\xC2\xA2" "\"\\u{a2}\"") 55 | (parse= "\xE2\x82\xAC" "\"\\u{20ac}\"") 56 | (parse= "\xF0\xA4\xAD\xA2" "\"\\u{24b62}\"") 57 | (parse= "\x7F" "\"\\u{7f}\"") 58 | (parse= "\xC2\x80" "\"\\u{80}\"") 59 | (parse= "\xDF\xBF" "\"\\u{7ff}\"") 60 | (parse= "\xE0\xA0\x80" "\"\\u{800}\"") 61 | (parse= "\xEF\xBF\xBF" "\"\\u{ffff}\"") 62 | (parse= "\xF0\x90\x80\x80" "\"\\u{10000}\"") 63 | (parse= "\xF4\x8F\xBF\xBF" "\"\\u{10ffff}\"")) 64 | 65 | (fn test-comments [] 66 | (let [(ok? ast) ((fennel.parser (fennel.string-stream ";; abc") 67 | "" {:comments true}))] 68 | (t.is ok?) 69 | (t.= :table (type (utils.comment? ast))) 70 | (t.= ";; abc" (tostring ast))) 71 | (let [code "{;; one\n1 ;; hey\n2 ;; what\n:is \"up\" ;; here\n}" 72 | (ok? ast) ((fennel.parser (fennel.string-stream code) 73 | "" {:comments true})) 74 | mt (getmetatable ast)] 75 | (== mt.comments 76 | {:keys {:is [(fennel.comment ";; what")] 77 | 1 [(fennel.comment ";; one")]} 78 | :values {2 [(fennel.comment ";; hey")]} 79 | :last [(fennel.comment ";; here")]}) 80 | (t.= mt.keys [1 :is]) 81 | (t.is ok?)) 82 | (let [code (table.concat ["{:this table" 83 | ";; has a comment" 84 | ";; with multiple lines in it!!!" 85 | ":and \"we don't want to lose the comments\"" 86 | ";; so let's keep em; all the comments are" 87 | ": good ; and we want them to be kept" 88 | "}"] "\n") 89 | (ok? ast) ((fennel.parser (fennel.string-stream code) 90 | "" {:comments true}))] 91 | (t.is ok? ast) 92 | (== (. (getmetatable ast) :comments :keys) 93 | {:and [(fennel.comment ";; has a comment") 94 | (fennel.comment ";; with multiple lines in it!!!")] 95 | :good [(fennel.comment ";; so let's keep em; all the comments are")]}) 96 | (== (. (getmetatable ast) :comments :last) 97 | [(fennel.comment "; and we want them to be kept")])) 98 | (let [(_ ast) ((fennel.parser "(do\n; a\n(print))" "-" {:comments true}))] 99 | (== ["do" "; a" "(print)"] (icollect [_ x (ipairs ast)] (tostring x))) 100 | ;; top-level version 101 | (== ["do" "; a" "(print)"] 102 | (icollect [_ x (fennel.parser ":do\n; a\n(print)" "-" {:comments true})] 103 | (tostring x))))) 104 | 105 | (fn test-control-codes [] 106 | (for [i 1 31] 107 | (let [code (.. "\"" (string.char i) (tostring i) "\"") 108 | expected (.. (string.char i) (tostring i))] 109 | (t.= (fennel.eval code) expected 110 | (.. "Failed to parse control code " i))))) 111 | 112 | (fn test-prefixes [] 113 | (let [code "\n\n`(let\n ,abc #(+ 2 3))" 114 | (ok? ast) ((fennel.parser code))] 115 | (t.is ok?) 116 | (t.= ast.line 3) 117 | (t.= (. ast 2 2 :line) 4) 118 | (t.= (. ast 2 3 :line) 4))) 119 | 120 | (fn line-col [{: line : col}] [line col]) 121 | 122 | (fn test-source-meta [] 123 | (let [code "\n\n ( let [x 5 \n y {:z 66}]\n (+ x y.z))" 124 | (ok? ast) ((fennel.parser code)) 125 | [let* [_ _ _ tbl]] ast 126 | [_ seq] ast] 127 | (t.is ok?) 128 | (t.= (line-col ast) [3 2] "line and column on lists") 129 | (t.= (line-col let*) [3 5] "line and column on symbols") 130 | (t.= (line-col (getmetatable seq)) [3 9] 131 | "line and column on sequences") 132 | (t.= (line-col (getmetatable tbl)) [4 10] 133 | "line and column on tables")) 134 | (let [code "abc\nxyz" 135 | parser (fennel.parser code) 136 | (ok? abc) (parser) 137 | (ok2? xyz) (parser)] 138 | (t.is (and ok? ok2?)) 139 | (t.= (tostring abc) (code:sub abc.bytestart abc.byteend)) 140 | (t.= (tostring xyz) (code:sub xyz.bytestart xyz.byteend)) 141 | ;; but wait! sub is tolerant of going on beyond the last byte! 142 | (t.= (length code) xyz.byteend)) 143 | ;; now let's try that again with tables 144 | (let [code "[1]\n{a 4} (true)" 145 | parser (fennel.parser code) 146 | (ok? seq) (parser) 147 | (ok2? kv) (parser) 148 | (ok3? list) (parser) 149 | seq-source (getmetatable seq) 150 | kv-source (getmetatable kv)] 151 | (t.is (and ok? ok2? ok3?)) 152 | (t.= (fennel.view seq) (code:sub seq-source.bytestart seq-source.byteend)) 153 | (t.= (fennel.view kv) (code:sub kv-source.bytestart kv-source.byteend)) 154 | (t.= (fennel.view list) (code:sub list.bytestart list.byteend)) 155 | ;; but wait! sub is tolerant of going on beyond the last byte! 156 | (t.= (length code) list.byteend)) 157 | (let [code " # " 158 | (ok? ast) ((fennel.parser code))] 159 | (t.is ok?) 160 | ; (t.= (line-col ast [1 3])) 161 | (t.= (tostring ast) (code:sub ast.bytestart ast.byteend)))) 162 | 163 | 164 | (fn test-plugin-hooks [] 165 | (var parse-error-called nil) 166 | (let [code "(there is a parse error here ((((" 167 | plugin {:versions [(fennel.version:gsub "-dev" "")] 168 | :parse-error #(set parse-error-called true)}] 169 | (t.is (not (pcall (fennel.parser code "" {:plugins [plugin]}))) 170 | "parse error is expected") 171 | (t.is parse-error-called "plugin wasn't called"))) 172 | 173 | {: test-basics 174 | : test-spicy-numbers 175 | : test-control-codes 176 | : test-comments 177 | : test-prefixes 178 | : test-source-meta 179 | : test-escapes 180 | : test-plugin-hooks} 181 | -------------------------------------------------------------------------------- /test/plugin/lua-plugin.lua: -------------------------------------------------------------------------------- 1 | -- inject some macros into rootScope 2 | local fennel = require('fennel') 3 | 4 | local function inscope(s) 5 | return _G['in-scope?'](s) 6 | end 7 | 8 | local rootScope = fennel.scope() 9 | while rootScope.parent do rootScope = rootScope.parent end 10 | 11 | rootScope.macros['is-in-scope'] = inscope 12 | 13 | return {name = "in-scope-lua-plugin", versions = "^1.5"} 14 | -------------------------------------------------------------------------------- /test/quoting.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | (local fennel (require :fennel)) 3 | (local view (require :fennel.view)) 4 | 5 | (macro v [expr] (view expr)) 6 | (macro peval [expr ?opts] 7 | `(pcall fennel.eval (v ,expr) ,?opts)) 8 | 9 | (fn c [code] 10 | (fennel.compileString code {:allowedGlobals false :compiler-env _G})) 11 | 12 | (fn cv [code] 13 | (view ((fennel.loadCode (c code) (let [env {:sequence fennel.sequence}] 14 | (set env._G env) 15 | (setmetatable env {:__index _G})))) 16 | {:one-line? true})) 17 | 18 | (fn test-quote [] 19 | (t.= (c "`:abcde") "return \"abcde\"" "simple string quoting") 20 | (t.= (cv "`[1 2 ,(+ 1 2) 4]") "[1 2 3 4]" 21 | "unquote inside quote leads to evaluation") 22 | (t.= (cv "(let [a (+ 2 3)] `[:hey ,(+ a a)])") "[\"hey\" 10]" 23 | "unquote inside other forms") 24 | (t.= (cv "`[:a :b :c]") "[\"a\" \"b\" \"c\"]" 25 | "quoted sequential table") 26 | (local viewed (cv "`{:a 5 :b 9}")) 27 | (t.is (or (= viewed "{:a 5 :b 9}") (= viewed "{:b 9 :a 5}")) 28 | (.. "quoted keyed table: " viewed)) 29 | 30 | 31 | ;; make sure shadowing the macro env in a macro body doesn't break anything 32 | (let [shadow-scope (fennel.scope) 33 | _ (fennel.compile-string 34 | (v (macro shadow-macro [name args ...] 35 | (let [g (. (getmetatable _G) :__index) 36 | shadow-bind []] 37 | (each [k (pairs _G)] 38 | (when (and (= :string (type k)) (not= k :_G) (= nil (. g k)) 39 | ;; `comment` shadows a special form 40 | (not= :comment k)) 41 | (table.insert shadow-bind (sym k)) 42 | (table.insert shadow-bind true))) 43 | `(macro ,name ,args (let ,shadow-bind ,...))))) 44 | {:scope shadow-scope}) 45 | (ok res) (peval (do (shadow-macro m [v] `(do ,v)) 46 | (shadow-macro n [v] `[,v]) 47 | (shadow-macro o [v] `(let [x# ,v] x#)) 48 | [(m :a) (n :b) (o :c)]) 49 | {:scope (fennel.scope shadow-scope)})] 50 | (t.is ok (: "shadowing the compiler env in a macro doesn't break quoting\n%s" 51 | :format (tostring res))) 52 | (t.= [:a [:b] :c] res 53 | "shadowing the compiler env in a macro doesn't break quoting"))) 54 | 55 | (fn test-quoted-source [] 56 | (t.= "return 3" (c "\n\n(eval-compiler (. `abc :line))") 57 | "syms have source data") 58 | (t.= "return 2" (c "\n(eval-compiler (. `abc# :line))") 59 | "autogensyms have source data") 60 | (t.= "return 4" (c "\n\n\n(eval-compiler (. `(abc) :line))") 61 | "lists have source data") 62 | (let [(_ msg) (pcall c "\n\n\n\n(macro abc [] `(fn [... a#] 1)) (abc)")] 63 | (t.match "unknown:5" msg "quoted tables have source data")) 64 | ;; runtime quoting 65 | (t.= "return {\"foo\", \"x\", table.unpack({1, 2, 3}), \"y\"}" 66 | (c "`[:foo :x ,(table.unpack [1 2 3]) :y]"))) 67 | 68 | (macro not-equal-gensym [] 69 | (let [s (gensym :sym)] 70 | `(let [,s 10 sym# 20] (and sym# (not= ,s sym#))))) 71 | 72 | (fn test-autogensym [] 73 | (t.is (not-equal-gensym))) 74 | 75 | {: test-quote 76 | : test-quoted-source 77 | : test-autogensym} 78 | -------------------------------------------------------------------------------- /test/relative-chained-mac-mod-mac.fnl: -------------------------------------------------------------------------------- 1 | (import-macros {: rsym} (.. ... :.mac-head)) 2 | 3 | (rsym c b a) 4 | -------------------------------------------------------------------------------- /test/relative-chained-mac-mod-mac/mac-head.fnl: -------------------------------------------------------------------------------- 1 | ;; relatively require a *module* for functionality in processing macro args 2 | (local relrequire ((fn [ddd] 3 | (fn [modname] 4 | (let [prefix (or (string.match ddd "(.+%.)mac%-head") "")] 5 | (require (.. prefix modname))))) ...)) 6 | 7 | (local {: bkwd} (relrequire :mod-mid)) 8 | 9 | (fn rsym [...] 10 | "generates a list of strings from a given list of symbols, in reverse" 11 | (let [syms [...] 12 | rsyms (icollect [v (bkwd syms)] (tostring v))] 13 | rsyms)) 14 | 15 | {: rsym} 16 | 17 | -------------------------------------------------------------------------------- /test/relative-chained-mac-mod-mac/mac-tail.fnl: -------------------------------------------------------------------------------- 1 | {:dec (fn dec [x] `(- ,x 1))} 2 | -------------------------------------------------------------------------------- /test/relative-chained-mac-mod-mac/mod-mid.fnl: -------------------------------------------------------------------------------- 1 | ;; relatively require a macro from a module required by a macro 2 | (import-macros {: dec} (do (.. (or (string.match ... "(.+%.)mod%-mid") "") :mac-tail))) 3 | 4 | (fn bkwd [seq] 5 | (var i (+ (length seq) 1)) 6 | (fn iter [] 7 | (let [next-i (dec i) 8 | val (. seq next-i)] 9 | (when val 10 | (set i next-i) 11 | (values val i))))) 12 | 13 | {: bkwd} 14 | -------------------------------------------------------------------------------- /test/relative-filename.fnl: -------------------------------------------------------------------------------- 1 | (require-macros ((fn [mod fname] 2 | ;; this expression is evaluated by the compiler with 3 | ;; the current module and filename injected via ... 4 | ;; mod name is this module 5 | (assert (= mod :test.relative-filename)) 6 | ;; filename is this file 7 | (assert (or (= fname :./test/relative-filename.fnl) 8 | (= fname "./test\\relative-filename.fnl")) 9 | (.. "filename was incorrect: " fname)) 10 | ;; return a good value as we are not testing this. 11 | (values :test.relative.macros)) ...)) 12 | 13 | (import-macros {:inc i-inc} ((fn [mod fname] 14 | (assert (= mod :test.relative-filename)) 15 | (assert (or (= fname :./test/relative-filename.fnl) 16 | (= fname "./test\\relative-filename.fnl")) 17 | (.. "filename was incorrect: " fname)) 18 | ;; return a good value as we are not testing this. 19 | (values :test.relative.macros)) ...)) 20 | (+ (i-inc 0) (inc 0)) 21 | -------------------------------------------------------------------------------- /test/relative.fnl: -------------------------------------------------------------------------------- 1 | (import-macros {: inc} (.. ... ".macros")) 2 | 3 | (inc 2) 4 | -------------------------------------------------------------------------------- /test/relative/macros.fnl: -------------------------------------------------------------------------------- 1 | {:inc (fn [x] (+ x 1))} 2 | -------------------------------------------------------------------------------- /test/searcher.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | (local fennel (require :fennel)) 3 | 4 | (fn test-searcher-error-contains-fnl-files [] 5 | (let [(ok error) (pcall require :notreal)] 6 | (t.= ok false) 7 | (t.= (string.match error :notreal.fnl) :notreal.fnl))) 8 | 9 | (fn with-preserve-searchers [f] 10 | (let [searchers-tbl (or package.searchers package.loaders) 11 | old-searchers (icollect [_ s (ipairs searchers-tbl)] s)] 12 | (while (next searchers-tbl) (table.remove searchers-tbl)) 13 | (pcall f) 14 | (while (next searchers-tbl) (table.remove searchers-tbl)) 15 | (each [_ s (ipairs old-searchers)] 16 | (table.insert searchers-tbl s)))) 17 | 18 | (fn test-install [] 19 | (tset package.loaded :test.searcher nil) 20 | (with-preserve-searchers 21 | #(do (fennel.install {}) 22 | (t.is (pcall require :test.searcher))))) 23 | 24 | (fn test-searcher [] 25 | (t.= "./test/searcher.fnl" (fennel.search-module "test/searcher")) 26 | (t.= "./src/fennel.fnl" (fennel.search-module "src.fennel")) 27 | (t.= nil (fennel.search-module "test.bad.with.dots"))) 28 | 29 | {: test-searcher-error-contains-fnl-files 30 | : test-install 31 | : test-searcher} 32 | -------------------------------------------------------------------------------- /test/sourcemap.fnl: -------------------------------------------------------------------------------- 1 | (local t (require :test.faith)) 2 | (local fennel (require :fennel)) 3 | 4 | (fn test-traceback [] 5 | (let [{: outer} (require :test.mod.tracer) 6 | traceback (outer)] 7 | (t.match "tracer.fnl:4:" traceback) 8 | (t.match "tracer.fnl:9:" traceback))) 9 | 10 | ;; what a mess! 11 | (fn normalize [tbl nups-51] 12 | (if (and (= "Lua 5.1" _VERSION) (not _G.jit)) 13 | ;; these don't exist in 5.1 14 | (set (tbl.nups tbl.nparams tbl.isvararg) nups-51) 15 | _G.jit nil 16 | (set tbl.istailcall false)) 17 | (when (= "Lua 5.4" _VERSION) 18 | (set (tbl.ntransfer tbl.ftransfer) (values 0 0))) 19 | tbl) 20 | 21 | (fn test-getinfo [] 22 | (let [{: outer : info : nest : coro} (fennel.dofile "test/mod/tracer.fnl")] 23 | (t.= (normalize {:currentline -1 24 | :func outer 25 | :isvararg false 26 | :lastlinedefined 11 27 | :linedefined 8 28 | :namewhat "" 29 | :nparams 1 30 | :nups 1 31 | :short_src "test/mod/tracer.fnl" 32 | :source "@test/mod/tracer.fnl" 33 | :what "Fennel"} 1) 34 | (fennel.getinfo outer)) 35 | (t.= {:activelines {14 true 16 true} 36 | :lastlinedefined 16 37 | :linedefined 13 38 | :short_src "test/mod/tracer.fnl" 39 | :source "@test/mod/tracer.fnl" 40 | :what "Fennel"} 41 | (info)) 42 | (t.= {:linedefined 18 43 | :lastlinedefined 21 44 | :short_src "test/mod/tracer.fnl" 45 | :source "@test/mod/tracer.fnl" 46 | :what "Fennel"} 47 | (fennel.getinfo nest "S")) 48 | (let [c (coroutine.create coro)] 49 | (coroutine.resume c) 50 | (t.= (normalize {:currentline 24 51 | :func coro 52 | :isvararg false 53 | :lastlinedefined 25 54 | :linedefined 23 55 | :namewhat "" 56 | :nparams 0 57 | :nups (if _G.jit 0 1) ; ??? 58 | :short_src "test/mod/tracer.fnl" 59 | :source "@test/mod/tracer.fnl" 60 | :what "Fennel"} 0) 61 | (fennel.getinfo c 1))))) 62 | 63 | {: test-getinfo 64 | : test-traceback} 65 | -------------------------------------------------------------------------------- /values.md: -------------------------------------------------------------------------------- 1 | # Values of Fennel 2 | 3 | This document is an outline of the guiding design principles of Fennel. 4 | Fennel's community values are covered in the [code of conduct](https://fennel-lang.org/coc). 5 | 6 | ## Compile-time 7 | 8 | First and foremost is the notion that Fennel is a compiler with no 9 | runtime. This places somewhat severe limits on what we can accomplish, 10 | but it also creates a valuable sense of focus. We are of course very 11 | fortunate to be building on a language like Lua where the runtime 12 | semantics are for the most part excellent, and the areas upon which we 13 | improve can be identified at compile time. 14 | 15 | This means Fennel (the language) consists entirely of macros and 16 | special forms, and no functions. Fennel (the compiler) of course has 17 | plenty of functions in it, but they are for the most part not intended 18 | for use outside the context of embedding the compiler in another Lua 19 | program. 20 | 21 | The exception to this rule is `fennel.view` which can be used 22 | independently; it addresses a severe problem in Lua's runtime 23 | semantics where `tostring` on a table produces nearly-useless 24 | results. But this can be thought of as simply another library which 25 | happens to be included in the compiler. The `fennel.view` function is 26 | a prerequisite to having a useful repl. 27 | 28 | The repl of course is also a function you can call at runtime if you 29 | embed the compiler, but this is a special case that blurs the lines 30 | between runtime and compile time. After all, what is compile time 31 | except that subset of runtime during which the function being run 32 | happens to be a compiler? 33 | 34 | ## Transparency 35 | 36 | Well-written Lua programs exhibit an excellent sense of transparency 37 | largely due to how Lua leans on lexical scoping so predominantly. 38 | When you look at a good Lua program, you can tell exactly where any 39 | given identifier comes from just by following the basic rules of 40 | lexical scope. Badly-written Lua programs often use globals and do not 41 | have this property. 42 | 43 | With Fennel we try to take this even further by making globals an 44 | error by default. It's still possible to write programs that use 45 | globals using `_G` (indeed for Lua interop this sometimes cannot be 46 | avoided) but it should be very clear when this happens; it's not 47 | something that you would do by accident or due to laziness. 48 | 49 | One counter-example here is the deprecated `require-macros` form; it 50 | introduced new identifiers into the scope without making it clear what 51 | the names were. That is why it was replaced by the much clearer 52 | `import-macros`. The two below are equivalent, but one has hidden 53 | implicit scope changes and the other exhibits transparency: 54 | 55 | ```fennel 56 | (require-macros :my.macros) ; what did we introduce here? who knows! 57 | 58 | (import-macros {: transform-bar : skip-element} :my-macros) 59 | ``` 60 | 61 | Of course this comes at the cost of a little extra verbosity, but it 62 | is well worth it. In Fennel programs, you should never have a hard 63 | time answering the question "where did this come from?" 64 | 65 | ## Making mistakes obvious 66 | 67 | The most obvious legitimate criticism of Lua is that it makes it easy 68 | to set or read globals by accident simply by making a typo in the name 69 | of an identifier. This is easily fixed by requiring global access to 70 | be explicit; it's perhaps the most obvious way that Fennel tries to 71 | catch common mistakes. But there are others; for instance Fennel does 72 | not allow you to shadow the name of a special form with a local. It 73 | also doesn't allow you to omit the body from a `let` form like many 74 | other lisps do: 75 | 76 | ```fennel 77 | (fn abc [] 78 | (let [a 1 79 | b 2 80 | c (calculate-c)]) ; <- missing body! 81 | (+ a b c)) 82 | ``` 83 | 84 | This will be flagged as an error because the entire `let` form is 85 | closed after the call to `calculate-c` when the intent was clearly 86 | only to close the binding form. 87 | 88 | Another example would be that you can't call `set` on a local unless 89 | it is introduced using `var`. This means that if you have code which 90 | assumes the locals will remain the same and then go and mess with that 91 | assumption it is an error; you have to explicitly declare that 92 | assumption void first before you are permitted to violate it. 93 | 94 | This touches on a broader theme: it's easier to understand code when 95 | you can look at it and immediately know certain things will never 96 | happen. By excluding certain capabilities from the language, certain 97 | mistakes become impossible. 98 | 99 | For example, Fennel code will never use a block of memory after it has 100 | been freed, because `malloc` and `free` are not even part of its 101 | vocabulary. In languages with immutable data structures, it's 102 | impossible to have bugs which come from one piece of code making a 103 | change to data in a way that another function did not expect. Fennel 104 | does not have immutable data structures, but still we recognize that 105 | removing the ability to do things (or making them opt-in instead of 106 | opt-out) can significantly improve the resulting code. 107 | 108 | Other examples include the lack of `goto` and the lack of early 109 | returns. Or how if a loop terminates early, it will make this obvious by 110 | using an `&until` clause at the top of the loop; you don't have to 111 | read the entire loop body to search for a `break` as you would in Lua. 112 | 113 | ## Consistency and Distinction 114 | 115 | Older lisps overload parentheses to mean lots of different things; 116 | they are used for data lists, but they are also used to signify 117 | function and macro calls or to group key/value pairs together and 118 | around an entire group of key/value pairs in `let`. There are many 119 | other uses. 120 | 121 | Fennel overloads delimiters in a few ways, but the distinction should 122 | be visually clearer and much more limited by context. Parentheses 123 | almost always mean a function or macro call; the main exception is 124 | inside a binding form where it can be used to bind multiple 125 | values. The other exception is the now-deprecated `?` notation for 126 | pattern matching guards; it has been replaced by calling 127 | `where`. Square brackets usually indicate a sequential table, but in a 128 | macro they can indicate a binding form. Perhaps were Fennel rooted in 129 | a language richer in typographical delimiters than English, this 130 | overloading would not be necessary and every delimiter pair could have 131 | exactly one meaning. 132 | 133 | This is something Lua drops the ball on in a few places; it overloads 134 | one notation to mean different things. For instance, `for` in Lua can 135 | be used to numerically step from one number to another in a loop, or 136 | it can be used to step thru an iterator. Fennel separates this out 137 | into `for` to be used with numeric stepping and `each` which uses 138 | iterators. Another example is the table literal notation: Lua uses 139 | `{}` for sequential tables as well as key/value tables, while Fennel 140 | uses `[]` for sequential tables following more recent programming 141 | convention. 142 | 143 | Fennel uses notation in other ways to avoid ambiguity; for instance 144 | when `&as` was introduced in destructuring forms for giving access to 145 | the entire table, the `&` character was reserved so that it could not 146 | be used in identifiers. This also makes it easier to write macros 147 | which do similar things; now we have a way to indicate that a given 148 | symbol must have some meaning assigned to it other than being an 149 | identifier. 150 | 151 | --------------------------------------------------------------------------------