Skip to content

Commit d402102

Browse files
authored
Support Fennel. (#530)
This adds support for the Fennel programming language: https://fennel-lang.org I couldn't find much explanation for what the different lexer rules meant, so I based it off of Clojure's lexer since the two languages share a very similar syntax. I also included a program to generate a list of keywords from Fennel's own listing, which will make it easier to update in the future.
1 parent 21a1750 commit d402102

File tree

3 files changed

+713
-0
lines changed

3 files changed

+713
-0
lines changed

Diff for: lexers/f/fennel.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package f
2+
3+
import (
4+
. "github.com/alecthomas/chroma" // nolint
5+
"github.com/alecthomas/chroma/lexers/internal"
6+
)
7+
8+
// Fennel lexer.
9+
var Fennel = internal.Register(MustNewLazyLexer(
10+
&Config{
11+
Name: "Fennel",
12+
Aliases: []string{"fennel", "fnl"},
13+
Filenames: []string{"*.fennel"},
14+
MimeTypes: []string{"text/x-fennel", "application/x-fennel"},
15+
},
16+
fennelRules,
17+
))
18+
19+
// Here's some Fennel code used to generate the lists of keywords:
20+
// (local fennel (require :fennel))
21+
//
22+
// (fn member? [t x] (each [_ y (ipairs t)] (when (= y x) (lua "return true"))))
23+
//
24+
// (local declarations [:fn :lambda :λ :local :var :global :macro :macros])
25+
// (local keywords [])
26+
// (local globals [])
27+
//
28+
// (each [name data (pairs (fennel.syntax))]
29+
// (if (member? declarations name) nil ; already populated
30+
// data.special? (table.insert keywords name)
31+
// data.macro? (table.insert keywords name)
32+
// data.global? (table.insert globals name)))
33+
//
34+
// (fn quoted [tbl]
35+
// (table.sort tbl)
36+
// (table.concat (icollect [_ k (ipairs tbl)]
37+
// (string.format "`%s`" k)) ", "))
38+
//
39+
// (print :Keyword (quoted keywords))
40+
// (print :KeywordDeclaration (quoted declarations))
41+
// (print :NameBuiltin (quoted globals))
42+
43+
func fennelRules() Rules {
44+
return Rules{
45+
"root": {
46+
{`;.*$`, CommentSingle, nil},
47+
{`\s+`, Whitespace, nil},
48+
{`-?\d+\.\d+`, LiteralNumberFloat, nil},
49+
{`-?\d+`, LiteralNumberInteger, nil},
50+
{`0x-?[abcdef\d]+`, LiteralNumberHex, nil},
51+
{`"(\\\\|\\"|[^"])*"`, LiteralString, nil},
52+
{`'(?!#)[\w!$%*+<=>?/.#-]+`, LiteralStringSymbol, nil},
53+
{`\\(.|[a-z]+)`, LiteralStringChar, nil},
54+
{`::?#?(?!#)[\w!$%*+<=>?/.#-]+`, LiteralStringSymbol, nil},
55+
{"~@|[`\\'#^~&@]", Operator, nil},
56+
{Words(``, ` `, `#`, `%`, `*`, `+`, `-`, `->`, `->>`, `-?>`, `-?>>`, `.`, `..`, `/`, `//`, `:`, `<`, `<=`, `=`, `>`, `>=`, `?.`, `^`, `accumulate`, `and`, `band`, `bnot`, `bor`, `bxor`, `collect`, `comment`, `do`, `doc`, `doto`, `each`, `eval-compiler`, `for`, `hashfn`, `icollect`, `if`, `import-macros`, `include`, `length`, `let`, `lshift`, `lua`, `macrodebug`, `match`, `not`, `not=`, `or`, `partial`, `pick-args`, `pick-values`, `quote`, `require-macros`, `rshift`, `set`, `set-forcibly!`, `tset`, `values`, `when`, `while`, `with-open`, `~=`), Keyword, nil},
57+
{Words(``, ` `, `fn`, `global`, `lambda`, `local`, `macro`, `macros`, `var`, `λ`), KeywordDeclaration, nil},
58+
{Words(``, ` `, `_G`, `arg`, `assert`, `bit32`, `bit32.arshift`, `bit32.band`, `bit32.bnot`, `bit32.bor`, `bit32.btest`, `bit32.bxor`, `bit32.extract`, `bit32.lrotate`, `bit32.lshift`, `bit32.replace`, `bit32.rrotate`, `bit32.rshift`, `collectgarbage`, `coroutine`, `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `debug`, `debug.debug`, `debug.gethook`, `debug.getinfo`, `debug.getlocal`, `debug.getmetatable`, `debug.getregistry`, `debug.getupvalue`, `debug.getuservalue`, `debug.sethook`, `debug.setlocal`, `debug.setmetatable`, `debug.setupvalue`, `debug.setuservalue`, `debug.traceback`, `debug.upvalueid`, `debug.upvaluejoin`, `dofile`, `error`, `getmetatable`, `io`, `io.close`, `io.flush`, `io.input`, `io.lines`, `io.open`, `io.output`, `io.popen`, `io.read`, `io.tmpfile`, `io.type`, `io.write`, `ipairs`, `load`, `loadfile`, `loadstring`, `math`, `math.abs`, `math.acos`, `math.asin`, `math.atan`, `math.atan2`, `math.ceil`, `math.cos`, `math.cosh`, `math.deg`, `math.exp`, `math.floor`, `math.fmod`, `math.frexp`, `math.ldexp`, `math.log`, `math.log10`, `math.max`, `math.min`, `math.modf`, `math.pow`, `math.rad`, `math.random`, `math.randomseed`, `math.sin`, `math.sinh`, `math.sqrt`, `math.tan`, `math.tanh`, `module`, `next`, `os`, `os.clock`, `os.date`, `os.difftime`, `os.execute`, `os.exit`, `os.getenv`, `os.remove`, `os.rename`, `os.setlocale`, `os.time`, `os.tmpname`, `package`, `package.loadlib`, `package.searchpath`, `package.seeall`, `pairs`, `pcall`, `print`, `rawequal`, `rawget`, `rawlen`, `rawset`, `require`, `select`, `setmetatable`, `string`, `string.byte`, `string.char`, `string.dump`, `string.find`, `string.format`, `string.gmatch`, `string.gsub`, `string.len`, `string.lower`, `string.match`, `string.rep`, `string.reverse`, `string.sub`, `string.upper`, `table`, `table.concat`, `table.insert`, `table.maxn`, `table.pack`, `table.remove`, `table.sort`, `table.unpack`, `tonumber`, `tostring`, `type`, `unpack`, `xpcall`), NameBuiltin, nil},
59+
{`(?<=\()(?!#)[\w!$%*+<=>?/.#-]+`, NameFunction, nil},
60+
{`(?!#)[\w!$%*+<=>?/.#-]+`, NameVariable, nil},
61+
{`(\[|\])`, Punctuation, nil},
62+
{`(\{|\})`, Punctuation, nil},
63+
{`(\(|\))`, Punctuation, nil},
64+
},
65+
}
66+
}

Diff for: lexers/testdata/fennel.actual

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
;; An example of some possible linters using Fennel's --plugin option.
2+
3+
;; The first two linters here can only function on static module
4+
;; use. For instance, this code can be checked because they use static
5+
;; field access on a local directly bound to a require call:
6+
7+
;; (local m (require :mymodule))
8+
;; (print m.field) ; fails if mymodule lacks a :field field
9+
;; (print (m.function 1 2 3)) ; fails unless mymodule.function takes 3 args
10+
11+
;; However, these cannot:
12+
13+
;; (local m (do (require :mymodule)) ; m is not directly bound
14+
;; (print (. m field)) ; not a static field reference
15+
;; (let [f m.function]
16+
;; (print (f 1 2 3)) ; intermediate local, not a static field call on m
17+
18+
;; Still, pretty neat, huh?
19+
20+
;; This file is provided as an example and is not part of Fennel's public API.
21+
22+
(fn save-require-meta [from to scope]
23+
"When destructuring, save module name if local is bound to a `require' call.
24+
Doesn't do any linting on its own; just saves the data for other linters."
25+
(when (and (sym? to) (not (multi-sym? to)) (list? from)
26+
(sym? (. from 1)) (= :require (tostring (. from 1)))
27+
(= :string (type (. from 2))))
28+
(let [meta (. scope.symmeta (tostring to))]
29+
(set meta.required (tostring (. from 2))))))
30+
31+
(fn check-module-fields [symbol scope]
32+
"When referring to a field in a local that's a module, make sure it exists."
33+
(let [[module-local field] (or (multi-sym? symbol) [])
34+
module-name (-?> scope.symmeta (. (tostring module-local)) (. :required))
35+
module (and module-name (require module-name))]
36+
(assert-compile (or (= module nil) (not= (. module field) nil))
37+
(string.format "Missing field %s in module %s"
38+
(or field :?) (or module-name :?)) symbol)))
39+
40+
(fn arity-check? [module] (-?> module getmetatable (. :arity-check?)))
41+
42+
(fn arity-check-call [[f & args] scope]
43+
"Perform static arity checks on static function calls in a module."
44+
(let [arity (# args)
45+
last-arg (. args arity)
46+
[f-local field] (or (multi-sym? f) [])
47+
module-name (-?> scope.symmeta (. (tostring f-local)) (. :required))
48+
module (and module-name (require module-name))]
49+
(when (and (arity-check? module) _G.debug _G.debug.getinfo
50+
(not (varg? last-arg)) (not (list? last-arg)))
51+
(assert-compile (= (type (. module field)) :function)
52+
(string.format "Missing function %s in module %s"
53+
(or field :?) module-name) f)
54+
(match (_G.debug.getinfo (. module field))
55+
{: nparams :what "Lua" :isvararg true}
56+
(assert-compile (<= nparams (# args))
57+
(: "Called %s.%s with %s arguments, expected %s+"
58+
:format f-local field arity nparams) f)
59+
{: nparams :what "Lua" :isvararg false}
60+
(assert-compile (= nparams (# args))
61+
(: "Called %s.%s with %s arguments, expected %s"
62+
:format f-local field arity nparams) f)))))
63+
64+
(fn check-unused [ast scope]
65+
(each [symname (pairs scope.symmeta)]
66+
(assert-compile (or (. scope.symmeta symname :used) (symname:find "^_"))
67+
(string.format "unused local %s" (or symname :?)) ast)))
68+
69+
{:destructure save-require-meta
70+
:symbol-to-expression check-module-fields
71+
:call arity-check-call
72+
;; Note that this will only check unused args inside functions and let blocks,
73+
;; not top-level locals of a chunk.
74+
:fn check-unused
75+
:do check-unused}

0 commit comments

Comments
 (0)