Language

Sytes implements a Scheme-like mini language to be used in templates. It has most features you'd expect from a Lisp—lexical scope, closures, macros—but it does not expose much of the host Lisp system. Templates should be kept simple.

In this section there are some notes about the language; to get familiar with the syntax you might want to read the syntax pages.

Sytes, template contexts

Sytes is designed to be able to run multiple websites on the same Lisp image. A Hunchentoot dispatcher checks the HOST from the HTTP request and selects the appropriate syte instance.

Each syte instance contains an execution context (let's name it *syte-context*) where one can define functions to be seen only by this syte. This context inherits from *toplevel-context* which is where most of the core language functions are defined. In turn, for each template that is compiled a new execution context is created, having *syte-context* as parent.

Compiled templates are cached in memory and will not be recompiled if the file wasn't modified since the time it was cached. A classic problem to be wary about is macros—if you modify a file containing macros, it will not automatically force a recompilation of all templates using those macros.

Variables

There are two ways to define variables in Sytes, and they're different even as top-level expressions:

(defglobal foo 10)
(define foo 10)

The first creates a global variable, while the second creates a lexically scoped one. As top-level definitions, both of them make the variable usable as if it were global, but the difference becomes important when you want to export functions from a template to use them in another. I'll get to this later.

define will create lexical variables in the most nested enclosing scope. Functions delimit scope. For example:

(define quad (lambda (x)
               (define x2 (* x x))
               ;; x2 is only accessible in this lambda
               (* x2 x2)))

(quad 2)  ==>  16

It also supports the Scheme-like syntax for defining functions:

(define (quad x)
  (define x2 (* x x))
  (* x2 x2))

Defining functions

As you can see above you can use define for defining functions, however, most of the times you will want your functions to be global. You can use defun for that:

{[defun say-hi (dude) {<p>Ciao {dude}!</p>}]}

Internally, defun uses defglobal to do its job.

Functions support Scheme-like variable argument lists.

Modules

Because each template runs in its own execution context, they can't share anything—not even global variables. Even if you have {(defglobal foo "the foo")}, that doesn't make “foo” available in other templates.

Globals however are treated as “exported” functionality when you include another template via require:

;; FILE: foo.syt
{[defglobal x "x from foo"]}
{[defun callme (y) {
   Here is x: {x}
   Here is y: {y}
}]}

;;-----------------------------------------

;; FILE: bar.syt
{[define foo (require "foo.syt")]}
{foo.x} <!-- displays "x from foo" -->
{(foo.callme "guess why")} <!-- calls the function -->

require looks up “foo.syt” (relative to the current template), compiles it if needed and executes it, but instead of returning its text output (which would be a bunch of newlines for the above example), it returns its context. Special magic wired into the parser allows us to use the dot notation to access global variables from a context.

Based on require we also have an import macro which makes selected symbols available as variables in the current context:

{[import "foo.syt" 'callme]}
{(callme "guess why")}

Compilation and evaluation

There are 3 distinct phases that combine to execute a template:

Unlike Common Lisp, you can't customize the Sytes reader (except for the .SYNTAX directive), but you have more control over the last two items: compilation and evaluation.

Because we support macros, the compiler needs to be able to execute macros at compilation time. Not much else is executed at this phase, so if macros use a function previously defined, for example, it won't be able to “see” it because the function begins to exist effectively after its definition is evaluated, which happens at the last phase.

A single special form allows you to control this: eval-when-compile. What follows might seem a little tricky (it took me quite some time to figure out Common Lisp's evaluation model), here's an example and following it you'll see the template output.

{[eval-when-compile
   (define seen 0)]}
{(set! seen (1+ seen))}

Output: 20

When you read this the number might not be 1. It'll actually increase at each page request. Try it, refresh this page! Also note that this number doesn't count your views, but all page views (all other visitors too). This is a little useless, but it serves to explain the function of eval-when-compile: its body is evaluated at the time the template is compiled (therefore, the variable is defined at phase “compilation” and its value set to zero). The following set! expression increases the current value of the variable and returns the new value; this is executed in phase “evaluation”, since it's outside the eval-when-compile body. Because the template is not compiled at each request, but only when it's changed, the value keeps increasing until the page is modified or the server restarted.

The use case for eval-when-compile is therefore to define stuff that is required at compile time, such as functions used by macros, but also it can be used as a means to optimize code. However, I would advise against using it if it's not really necessary.

Fork me on Github