Extending LIPS
There are two ways to extend LIPS Scheme, one is through macros and the other ways is with syntax extensions.
Macros
LIPS allow creating Lisp macros and Scheme hygienic macros. Right now the limitations of macros is that they are runtime. There are no expansion time. Macros act like function calls, but they transform the code and the interpreter evaluates the code that is returned by the macro. They ware implemented like this, because this is how I understood the macros when they first got implemented. There is a plan to create proper macro expansion.
Quasiquote works with object literals, like with vectors:
(let* ((x 10)
(y 20)
(obj `&(:x ,x :y ,y)))
(print obj))
to define a lisp macro, you use syntax defined in Scheme Tutorial about Macros.
(define-macro (for var start end . body)
`(for-each (lambda (,var)
,@body)
(range ,start ,(+ end 1))))
(let ((result (vector)))
(for i 10 20
(result.push i))
(print result))
;; ==> #(10 11 12 13 14 15 16 17 18 19 20)
You can define macro that create shorthand syntax like in JavaScript:
const x = 10;
const y = 20;
const obj = { x, y };
console.log(obj);
// { x: 10, y: 20 }
You can create macro that will work the same in LIPS Scheme:
(define (symbol->key symbol)
(string->symbol (string-append ":" (symbol->string symbol))))
(define-macro (expand . args)
`(object ,@(reduce (lambda (symbol acc)
(let ((key (symbol->key symbol)))
(append acc (list key symbol))))
'()
args)))
(let* ((x 10)
(y 20)
(obj (expand x y)))
(print obj))
;; ==> &(:x 10 :y 20)
Hygienic macros
LIPS define hygienic macros in form of standard syntax-rules
expression. Note that there are know
bugs in syntax-rules
see issue #43 on GitHub and unit
tests that have tests marked as
failing.
If you find a case of failing macro, don't hessitate to create an issue. You can also check if your case is not already listed on above links. You can also just create a comment on issue #43 with your broken test case.
LIPS Scheme define those extensions to syntax-rules macros:
- SRFI-46 (changing ellipsis symbol: see Nested Hygienic Macros
- SRFI-139 see Syntax Parameters
- SRFI 147 allow defining new syntax-rules transformers
Macroexpand
LIPS define macroexpand
and macroexpand-1
but they are macros and the expression don't need to be quoted.
There is an issue to change those expressions into functions like
in Common Lisp.
Syntax extensions
Syntax extensions are a way to add new syntax to LIPS Scheme. They are executed at parse time. Object literals and vector literals are added using syntax extensions. Syntax extension modify the Parser and allow to add new behavior at parse time.
To add syntax extension you use:
(set-special! "##" 'my-function lips.specials.LITERAL)
The syntax extension can point to a macro or a function. When extension is a function it's invoked and the result data is returned from the parser:
(define (my-function number)
`(list ,number ,number))
if you define the function like this and execute:
##10
;; ==> (10 10)
To see the expansion of syntax extension you can use lips.parse
:
(lips.parse "##10")
;; ==> #((list 10 10))
NOTE: The lips.parse
function return array/vector of parsed expressions.
There are 3 types of syntax extensions SPLICE
, LITERAL
, and SYMBOL
. You define them using
constants defined in lips.specials
object.
LITERAL
- used above pass it's argument as is, with literal syntax extension you can execute it on any argument. This is default when no constant inset-special!
is used.SPLICE
- if you execute syntax##(1 2 3)
the arguments will be spliced, so the function or a macro needs to use improper list. Or use named arguments if syntax accept fixed amount of arguments.SYMBOL
- this type of extensions don't accept any arguments and can be used to define parser constants.
Splice syntax extensions
(set-special! "##" 'complex lips.specials.SPLICE)
(define (complex real imag)
(make-rectangular real imag))
This syntax extension will define complex numbers and will work only on lists:
##(10 20)
;; ==> 10+20i
Since it's a macro it evaluate at parse time:
(lips.parse "##(10 20)")
;; ==> #(10+20i)
With splice syntax extension you can limit the number of arguments (remember that LIPS don't check arity).
(define (complex . args)
(if (not (= (length args) 2))
(throw "Invalid invocation of ## syntax extension")
(apply make-rectangular args)))
(lips.parse "##(10 20)")
;; ==> #(10+20i)
(lips.parse "##(1 2 3)")
;; ==> Invalid invocation of ## syntax extension
Symbol syntax extensions
The last type of syntax extensions are symbols they don't accept any arguments and can be used to define parser constants.
(set-special! "nil" 'nil-fn lips.specials.SYMBOL)
(define (nil-fn) '())
This will define constant #nil
. It's different from nil
variable:
(define nil '())
(eq? nil #nil)
;; ==> #t
(eq? (car '(nil)) (car '(#nil)))
;; ==> #f
(symbol? (car '(nil)))
;; ==> #f
(symbol? (car '(#nil)))
;; ==> #f
(eq? (car '(#nil)) '())
;; ==> #t
Autogensyms
With syntax extensions you can define autogensyms expressions:
(set-special! "#:" 'keyword lips.specials.LITERAL)
(define (keyword symbol)
`(gensym ',symbol))
(let ((x #:foo))
(write x))
;; ==> #:foo
This allow to create named gensyms that are unique:
(eq? #:foo #:foo)
;; ==> #f
You can use them with lisp macros instead of gensym
expressions. The autogensyms are actually part
of the standard library.
String interpolation
With syntax extensions you can create string interpolation that expand into a Scheme code:
(set-special! "$" 'interpolate)
(define (interpolate str)
(typecheck "interpolate" str "string")
(let* ((re #/(\$\{[^\}]+\})/)
(parts (--> str (split re) (filter Boolean))))
`(string-append ,@(map (lambda (part)
(if (not (null? (part.match re)))
(let* ((expr (part.replace #/(^\$\{)|(\}$)/g ""))
(port (open-input-string expr))
(value (with-input-from-port port read)))
`(repr ,value))
part))
(vector->list parts)))))
(pprint (macroexpand-1 (let ((x 10)) $"x = ${(+ x 2)}")))
;; ==> (let ((x 10))
;; ==> (string-append "x = " (repr (+ x 2))))
(let ((x 10))
$"x = ${(+ x 2)}")
;; ==> "x = 12"
The limitation of this solution is that you can't use strings inside ${ ... }
. It will break the
Lexer. In order to have full string interpolation you need to read the parser stream (See Standard
input inside syntax extensions).
Accessing parser
In LIPS syntax extensions you can access the parser instance, so you can implement syntax extension that return line number:
(set-special! "#:num" 'line-num lips.specials.SYMBOL)
(define (line-num)
(let* ((lexer lips.__parser__.__lexer__)
(token lexer.__token__))
(write token)
(newline)
;; line number start from 0
(+ token.line 1)))
(print (list
#:num
#:num))
;; ==> &(:token "#:num" :col 8 :offset 260 :line 11)
;; ==> &(:token "#:num" :col 10 :offset 274 :line 12)
;; ==> (12 13)
NOTE: The provided output will be exactly the same, when the code will be put into a single file and executed.
Standard input
In syntax extensions current-input-port
points into the parser stream. So you can implement
your own parser logic. The best way to implement custom syntax extension (that works similar to
common lips reader macros).
(set-special! "$" 'raw-string lips.specials.SYMBOL)
(define (raw-string)
(if (char=? (peek-char) #\")
(begin
(read-char)
(let loop ((result (vector)) (char (peek-char)))
(read-char)
(if (char=? char #\")
(apply string (vector->list result))
(loop (vector-append result (vector char)) (peek-char)))))))
(print $"foo \ bar")
;; ==> "foo \\ bar"
This extension implements raw string, like in Python, where you don't need to escape the characters that are thread literally.
Similarly, you can implement strings that use backticks, you only need to replace #\"
with #\`
.
(set-special! "$" 'raw-string lips.specials.SYMBOL)
(define (raw-string)
(if (char=? (peek-char) #\`)
(begin
(read-char)
(let loop ((result (vector)) (char (peek-char)))
(read-char)
(if (char=? char #\`)
(apply string (vector->list result))
(loop (vector-append result (vector char)) (peek-char)))))))
(print $`foo \ bar`)
;; ==> "foo \\ bar"
With this feature in hand you can implement full string interpolation (that will probably be part of LIPS Scheme in the future).
Limitations
The limitation of syntax extensions is that you can't define a variable that starts with the same characters as syntax extension. This may be a benefit and not a limitation:
New Homoiconic data types
With LIPS, you can define representation of custom data types that are the same when printed and read.
To create custom representation of new data type you can use set-repr!
expression. It only works
with JavaScript classes. But Scheme records in LIPS define new JavaScript class. So you can create
new records and create different representation for them.
(define-record-type :Person
(make-person name age)
person?
(name person-name set-name!)
(age person-age set-age!))
(set-repr! :Person (lambda (obj quot)
(string-append "(make-person "
(repr (person-name obj) quot)
" "
(repr (person-age obj) quot)
")")))
(write (make-person "Mick Jagger" 80))
;; ==> (make-person "Mick Jagger" 80)
(display (make-person "Mick Jagger" 80))
;; ==> (make-person Mick Jagger 80)
As you can see the display
don't quote the strings because of repr
expression that use quot
argument to the set-repr!
handler.
Combining with syntax extensions
You can combine syntax extensions with custom representation:
(set-special! ":P" 'make-person lips.specials.SPLICE)
(set-repr! :Person (lambda (obj quot)
(string-append ":P("
(repr (person-name obj) quot)
" "
(repr (person-age obj) quot)
")")))
(write :P("Mick Jagger" 80))
;; ==> :P("Mick Jagger" 80)