lineva

2022-11-07

Linear evaluation macro system

Upstream URL

github.com/dzangfan/lineva

Author

Li Dzangfan <li-fn@outlook.com>

License

GPLv3
README

1Linear Evaluation Macro System

1.1Introduction

  (defun read-first-line (file-name &optional required)
    (la:leva
      (:check-type (file-name (or (array character *) pathname)))
      (:let (stream (open file-name)))
      (:defer (close stream))
      (:let (first-line (read-line stream nil)))
      (:return first-line :if (or (not required) first-line))
      (error "File ~A is empty" file-name)))

  ;; Expand to

  (defun read-first-line (file-name &optional required)
    (progn
      (check-type file-name (or (array character *) pathname))
      (let ((stream (open file-name)))
        (unwind-protect
             (let ((first-line (read-line stream nil)))
               (if (or (not required) first-line)
                   first-line
                   (error "File ~A is empty" file-name)))
          (close stream)))))

Although S-exp is simple, consistent and unambiguous, we do need non-S-exp feature in our code sometime. For example, in most programming languages like C, we can easily write

  char* file_name = get_file_name(stdin);
  assert(file_name != NULL);
  FILE* handle = open_file(file_name);
  assert(handle != NULL);
  Error* error = process(handle);
  if (error != NULL) return FLAG_ERROR;
  return FLAG_OK;

However, in Lisp, we have to write following code without defining macro:

  (let ((file-name (get-file-name stdin)))
    (assert file-name)
    (let ((handle (open-file file-name)))
      (assert handle)
      (let ((err (process handle)))
        (if err
            :error
            :ok))))

Although the logic is simple, as the indentation increase and the forms nest with others, the code become somewhat complex. This package provides a C-like sequential calculation.

  (la:leva
    (:let (file-name (get-file-name stdin)))
    (assert file-name)
    (:let (handle (open-file file-name)))
    (assert handle)
    (:let (err (process handle)))
    (if err :error :ok))

Another example is local functions in Lisp. In Scheme, we use define to define a local function when the body of function is overlong, rather than let:

  (define (sort lst)
    (define (insert elt lst)
      (cond [(null? lst) (list elt)]
            [(< elt (car lst)) (cons elt lst)]
            [else (cons (car lst)
                        (insert elt (cdr lst)))]))
    (if (null? lst)
        lst
        (insert (car lst)
                (sort (cdr lst)))))

However, in Common Lisp, since defun always defines a global function instead of a local function, labels and flet are widely used. But the code can become cumbersome quickly as the local functions get longer. Therefore, a Scheme-like feature for defining local functions is available in this package:

  (defun sort (lst)
    (la:leva
      (:defun insert (elt lst)
        ...)
      (if (null lst) ...)))

As we saw, leva (stand for Linearly EVAluate) is the main operator of this package. It is generally a superset of progn: evaluate forms sequentially and treat forms led by keyword specially. Therefore, the title "Linear Evaluation Macro System" does not mean another evaluation rule or a replacement of current Lisp macro system, but a complement of it.

1.2Installation

Since leva does not depend on other libraries, you can simply download lineva.lisp and load it. To enable leva cooperate with a bigger system, clone this repository and load lineva.asd by asdf. For example, the simplest way is that define your own system and make it depend on lineva:

  ;; foo.asd

  (require :asdf)

  (in-package :asdf-user)

  (push "/path/to/lineva.lisp/" asdf:*central-registry*)

  (defsystem :foo
    :depends-on (:lineva)
    :components ())

Check document of asdf for more details.

1.3Use leva

leva is the main macro of this package, it is generally a progn. However, forms in top level of body of leva will be treat as a instruction. Formally, leva take any number of forms and each form have one of following shape:

  1. (:name . LAMBDA-LIST)
  2. :name, which is a equivalent of (:name)
  3. Any Lisp form except 1. and 2.

following forms are legal parameter of leva:

  • (:break "Bang!")
  • :break
  • (format t "See you space cb")

Incidentally, :break is a built-in instruction corresponding to standard function break in Common Lisp. Note that a instruction is always a keyword, which allows leva distinguish normal lisp form and instructions. Instructions is valid only in top level of leva. So the following code is invalid:

  (la:leva
    (if (null lst)
        (:return lst)))

1.4Create a Instruction

A instruction generally works like macro, except

  1. only works in context of leva
  2. take rest part of evaluation as a parameter

The "rest part of evaluation" means code expanded from parameters of leva which follows current instruction. Take the following code for example:

  (la:leva
    (:let (x 1))
    (:let (y 2))
    (+ x y))

For instruction invocation (:let (y 2)), (+ x y) is its "rest code"; for invocation (:let (x 1)), code expanded from (:let (y 2)) is its "rest code". Normally, instruction should not ignore its "rest code".

Instructions are defined by definst, which is basically a equivalent of defmacro except a built-in variable $rest-code is visible in body of definition. For example, to define instruction let, we can write:

  (definst :let (&rest let-arguments)
    "Define local variables by `let'."
    `(let ,let-arguments ,$rest-code))

The first parameter is always a keyword. The second parameter is a lambda-list, which correspond to cdr part of instruction's invocation. Rest parameter is the macro body, which generates code like macro by implicit parameter $rest-code. By convention, if the first component of body is a literal string, it will be interpreted as a docstring of this instruction.

1.5Built-in Instructions

A number of instructions have been defined. Available instructions can be found by (la:available-instructions); detail usage of the instruction can be found by (la:describe-instruction :instruction).

1.5.1Local Variables

1.5.1.1:let

lambda-list: :LET (&REST LET-ARGUMENTS)

Define local variables by `let'. LET-ARGUMENTS has the same meaning of `let'.

(la:leva 
  (:let (x 10) (y 20))
  (+ x y))

1.5.1.2:let-assert

lambda-list: :LET-ASSERT (&REST LET-ARGUMENTS)

Define local variables by `let' and assert its value. LET-ARGUMENTS has the same meaning of `let'.

(la:leva
(:let-assert (x 10) (y 20) (z nil))
(+ x y z))

1.5.1.3:flet

lambda-list: :FLET (&REST FLET-ARGUMENTS)

Define local function by `flet', FLET-ARGUMENTS has the same meaning with `flet'.

(la:leva
  (:flet (add1 (x) (+ 1 x))
         (dot2 (x) (* 2 x)))
  (dot2 (add1 10)))

1.5.1.4:labels

lambda-list: :LABELS (&REST LABELS-ARGUMENTS)

Define local function by `labels'. LABELS-ARGUMENTS has the same meaning with `labels'

(la:leva
  (:labels (fib (n)
                (if (< n 2)
                    1
                    (+ (fib (- n 1)) (fib (- n 2))))))
  (fib 5))

1.5.1.5:macrolet

lambda-list: :MACROLET (&REST MACROLET-ARGUMENTS)

Define local macro by `macrolet'. MACROLET-ARGUMENTS has the same meaning with `macrolet'.

(la:leva
  (:macrolet (record (&rest values) `(list ,@values)))
  (record "Joe" 20 nil))

1.5.1.6:symbol-macrolet

lambda-list: :SYMBOL-MACROLET (&REST SYMBOL-MACROLET-ARGUMENTS)

Define a local symbol-macro by `symbol-macrolet'.

SYMBOL-MACROLET-ARGUMENTS has the same meaning with`symbol-macrolet'.
(la:leva (:symbol-macrolet (x (format t "...~%")))
  (list x x x))

1.5.1.7:defun

lambda-list: :DEFUN (NAME LAMBDA-LIST &BODY BODY)

Define a local function by `labels'.

(la:leva
  (:defun fac (n)
    (if (zerop n)
        1
        (* n (fac (- n 1)))))
  (fac 3))

1.5.1.8:defvar

lambda-list: :DEFVAR (NAME &OPTIONAL VALUE)

Define a local variable by `let'.

(la:leva
  (:defvar x 10)
  x)

1.5.1.9:bind

lambda-list: :BIND (LAMBDA-LIST EXPRESSION)

Define local variables by `destructuring-bind'.

(la:leva
  (:bind (a b &rest c) '(1 2 3 4 5))
  (list a b c))

1.5.1.10:setf

lambda-list: :SETF (PLACE VALUE &KEY (IF T))

Invoke `setf' to set PLACE to VALUE if IF is not `nil'.

(la:leva
  (:defvar name :alexandria)
  (:setf name (symbol-name name)
         :if (not (stringp name)))
  name)

1.5.2Debug

1.5.2.1:break

lambda-list: :BREAK (&OPTIONAL FORMAT-CONTROL &REST FORMAT-ARGUMENTS)

Enter debugger by call `break'. Arguments has the same meaning with `break'.

(la:leva  
  (:break "Let's ~A!!!" :burn))

1.5.2.2:inspect

lambda-list: :INSPECT (OBJECT)

Enter inspector with OBJECT.

(la:leva
  (:defvar x '(:foo :bar))
  (:inspect x))

1.5.2.3:assert

lambda-list: :ASSERT (&REST CONDITIONS)

Quickly assert that all CONDITIONS is true.

(la:leva
  (:defvar x 10)
  (:assert (numberp x) (plusp x) (evenp x))
  x)

1.5.2.4:check-type

lambda-list: :CHECK-TYPE (&REST CHECK-TYPE-PARAMETERS)

Invoke `check-type' over each element of CHECK-TYPE-PARAMETERS.

(la:leva
  (:let (name "Joe") (age 20))
  (:check-type (name (array character *) "a string")
               (age (integer 0 150)))
  (list name age))

1.5.3Contro Flow

1.5.3.1:return

lambda-list: :RETURN (VALUE &KEY (IF T))

Return VALUE if condition IF is true.

(la:leva
  (:defvar x (read))
  (:return (- x) :if (minusp x))
  x)

1.5.3.2:try

lambda-list: :TRY (&REST VALUES)

Return first value in VALUES which is not `nil'. If all VALUES is `nil', evaluate rest code.

(la:leva
  (:defvar table
    '(:bing "cn.bing.com"))
  (:try (getf table :google)
        (getf table :duckduckgo)
        (getf table :bing))
  "No search engine available.")

1.5.3.3:defer

lambda-list: :DEFER (&REST FORMS)

Evaluate rest codes, then evaluate FORMS sequentially. Result of rest code will be returned. Evaluation of rest code will be protected by `unwind-protect'.

(la:leva
  (:defun close-conn () (format t "Bye!~%"))
  (format t "Hello!~%")
  (:defer (close-conn) (terpri))
  (format t "[...]~%"))

1.5.4Display

1.5.4.1:printf

lambda-list: :PRINTF (FORMAT-STRING &REST ARGUMENTS)

Print content to standard output. FORMAT-STRING and ARGUMENTS have the same meaning of `format'.

(la:leva (:printf "Hello ~S!~%" :world))

1.5.4.2:println

lambda-list: :PRINTLN (THING)

Print content to standard output and add newline. Use `princ' to output.

(la:leva (:println "Hello world!"))

1.5.4.3:pn

lambda-list: :PN (THING)

Print content to standard output and add newline. Use `prin1' to output.

(la:leva (:pn "Hello world!"))

Dependencies (1)

  • fiveam

Dependents (0)

    • GitHub
    • Quicklisp