Skip to content

Commit 0f9e2d0

Browse files
committed
Add macroRequire: easier loading from .esl files
With reference to #60 (comment) It is a common use-case to write eslisp macros in separate files. These are only used during compilation, and so it can be awkward to have a build system in place that compiles them to JavaScript first so you can compile your other code. To make this use a little more convenient, this commit introduces a macro called `macroRequire`, which can be given a path to a .esl file for macros to be loaded from. `macroRequire` is strictly less powerful than the existing `macro`-macro, in that it cannot be given an arbitrary expression from which to load a macro. However, while it has always been possible to emulate `macroRequire` using just `macro`, the required degree of familiarity with Node.js' internal module API made it difficult. Hence this sugar.
1 parent 0464e52 commit 0f9e2d0

5 files changed

+140
-8
lines changed

doc/basics-reference.markdown

+27-7
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,12 @@ generate arbitrary JavaScript are built in to eslisp.
184184

185185
#### Macro-related
186186

187-
| name | description |
188-
| ------------ | ------------------------------------- |
189-
| `macro` | macro directive |
190-
| `capmacro` | environment-capturing macro directive |
191-
| `quote` | quotation operator |
192-
| `quasiquote` | quasiquote |
187+
| name | description |
188+
| -------------- | ------------------------------------- |
189+
| `macro` | macro directive |
190+
| `macroRequire` | loads a macro from .esl file |
191+
| `quote` | quotation operator |
192+
| `quasiquote` | quasiquote |
193193

194194
These are only valid inside `quasiquote`:
195195

@@ -583,7 +583,27 @@ or `finally`, in which case they are treated as the catch- or finally-clause.
583583
Either the catch- or finally- or both clauses need to be present, but they can
584584
appear at any position. At the end is probably most readable.
585585

586-
## User-defined macros
586+
## Loading existing macros
587+
588+
From an .esl or .js file that exports a function:
589+
590+
(macroRequire macroName "./path/to/file.esl")
591+
592+
From an .esl or .js file that exports an object which properties you want to
593+
load as macros:
594+
595+
(macroRequire "./path/to/file.esl")
596+
597+
From an npm module that exports a function:
598+
599+
(macro macroName (require "module-name"))
600+
601+
From an npm module that exports an object which properties you want to load as
602+
macros:
603+
604+
(macro (require "module-name"))
605+
606+
## Defining your own macros
587607

588608
If you can think of any better way to write any of the above, or wish you could
589609
write something in a way that you can't in core eslisp, check out [how macros

readme.markdown

+5
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,11 @@ whatever, so you can put the macro function in a separate file and do—
439439

440440
—to use it.
441441

442+
Or if you'd like to also be able to load macros from `.esl` eslisp files
443+
without first compiling them to JavaScript, you can do that with
444+
445+
(macroRequire someName "./file.esl")
446+
442447
This means you can publish eslisp macros on [npm][38]. The name prefix
443448
`eslisp-` and keyword `eslisp-macro` are recommended. [Some exist
444449
already.][39]

src/built-in-macros.ls

+62
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ statementify = require \./es-statementify
77
} = require \./import-macro
88
Module = require \module
99
require! \path
10+
require! \fs
1011

1112
chained-binary-expr = (type, operator, associativity=\right) ->
1213
macro = ->
@@ -409,6 +410,66 @@ contents =
409410
handler : catch-clause
410411
finalizer : finally-clause
411412

413+
\macroRequire : ->
414+
env = this
415+
416+
# Load a macro from the specified file. If the file's extension is '.esl',
417+
# load it as eslisp code.
418+
419+
compile-file-as-macro = (file-name) ->
420+
# Read the file's contents, and if it's an eslisp file, compile it to JS.
421+
file-content = fs.read-file-sync file-name, \utf-8
422+
if file-name.ends-with '.esl'
423+
eslisp-to-js = require "./index"
424+
file-content := eslisp-to-js file-content
425+
426+
# Run the file as a new module.
427+
new-module = new Module file-name, module
428+
..paths = Module._node-module-paths file-name
429+
..filename = file-name
430+
new-module._compile file-content, file-name
431+
432+
return new-module.exports
433+
434+
switch &length
435+
case 1
436+
[ file-name ] = arguments
437+
unless file-name.type in <[ atom string ]>
438+
throw Error "Invalid require path: #{JSON.stringify file-name}"
439+
440+
file-name .= value
441+
442+
macro = compile-file-as-macro file-name
443+
switch typeof! macro
444+
| \Object =>
445+
for k, v of macro then env.import-macro k, v
446+
| \Function =>
447+
throw Error "Cannot load macro from function without name given"
448+
| otherwise =>
449+
throw Error "Invalid macro return value #{JSON.stringify macro}"
450+
451+
env.import-macro name, macro-func
452+
453+
case 2
454+
[ name, file-name ] = arguments
455+
unless name.type in <[ atom string ]>
456+
throw Error "Invalid macro name: #{JSON.stringify name}"
457+
unless file-name.type in <[ atom string ]>
458+
throw Error "Invalid require path: #{JSON.stringify file-name}"
459+
460+
name .= value
461+
file-name .= value
462+
463+
macro-func = compile-file-as-macro file-name
464+
env.import-macro name, macro-func
465+
466+
default
467+
throw Error """
468+
macroRequire: Unexpected number of arguments.
469+
Got #{&length}. Expected 1 or 2.
470+
"""
471+
return null
472+
412473
\macro : ->
413474
env = this
414475

@@ -441,6 +502,7 @@ contents =
441502

442503
let require = require-substitute
443504
eval "(#{env.compile-to-js es-ast})"
505+
444506
switch &length
445507
| 1 =>
446508
form = &0

src/compile.ls

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ list-to-estree = (env, { values }:ast, options={}) ->
166166
..?loc ||= head.location
167167

168168
| otherwise =>
169-
throw Error "Unexpected macro return type #that"
169+
throw Error "Unexpected macro return type from #{head.value }: #that"
170170

171171
else
172172
# Compile to a function call

test.ls

+45
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,51 @@ test "macro can handle new-ish JS features like ClassDeclaration" ->
11991199
'''
12001200
..`@equals` "class A {\n}"
12011201

1202+
if not running-in-browser
1203+
test "macroRequire can load a macro from .esl file" ->
1204+
{ name, fd } = tmp.file-sync postfix: \.esl
1205+
fs.write-sync fd, '''
1206+
(= (. module exports) (lambda (x) (return `(hiFromOtherFile ,x))))
1207+
'''
1208+
esl """
1209+
(macroRequire test "#name")
1210+
(test asd)
1211+
"""
1212+
..`@equals` "hiFromOtherFile(asd);"
1213+
1214+
fs.unlink-sync name # clean up
1215+
1216+
test "macroRequire can load macros from .esl file (as object)" ->
1217+
{ name, fd } = tmp.file-sync postfix: \.esl
1218+
fs.write-sync fd, '''
1219+
(= (. module exports)
1220+
(object a (lambda (x) (return 'a))
1221+
b (lambda (x) (return 'b))))
1222+
'''
1223+
esl """
1224+
(macroRequire "#name")
1225+
(a)
1226+
(b)
1227+
"""
1228+
..`@equals` "a;\nb;"
1229+
1230+
fs.unlink-sync name # clean up
1231+
1232+
test "macroRequire can load a macro from .js file outputting estree" ->
1233+
{ name, fd } = tmp.file-sync postfix: \.js
1234+
fs.write-sync fd, '''
1235+
module.exports = function () {
1236+
return { type: "Identifier", name: "hiFromOtherFile" }
1237+
}
1238+
'''
1239+
esl """
1240+
(macroRequire test "#name")
1241+
(test asd)
1242+
"""
1243+
..`@equals` "hiFromOtherFile;"
1244+
1245+
fs.unlink-sync name # clean up
1246+
12021247
test "multiple invocations of the compiler are separate" ->
12031248
esl "(macro what (lambda () (return 'hi)))"
12041249
esl "(what)"

0 commit comments

Comments
 (0)