Lispy

Sarnold 2009-09-20 - Yet another week-end fun project: parsing S-EXP to Tcl.

Usage:

 lispy::parse string
% source lispy.tcl
% lispy::parse {puts (* (math.atan -1) 4)}
puts [expr {atan(-1)*4}]

Lispy syntax

Each command starts with a command name and continues with arguments. A simple word (which starts with a letter or a colon) is taken as value (token becomes $token) if it is not the first word of the command. As in Lisp, parentheses match nested commands:

 add (mul x y) (mul z t)
 -> add [mul $x $y] [mul $z $t]

To give expressions, you can use the following commands/functions:

 * + / - % ** < <= == != > >= >> << & && | || ^ ~ ! lt? gt? eq? ne? in? ni? ge? le?

and the math functions defined by expr are accessed via math.funcname

 (+ x (math.sin y)) => expr {$x+sin(y)}

Expressions expected by commands such as if, while, etc. are to be prepended with a question mark:

 (if ?(< x 0) (begin (puts "x is negative")))

Numbers are put verbatim

 1.2 -> 1.2

Options are put verbatim, as with command names starting with special chars.

 -textvariable
 ++
 *

To start the command with a variable value, you will have to put a dollar sign in front of it.

 $widget 'add 'circle 1 1 10 10
 -> $widget add circle 1 1 10 10

String interpolation is possible with double-quoted strings which are put verbatim.

 puts "Hello my $friend"

Lists are built with (single) quotes in front of parenthesized expressions, and can be nested.

 lindex '(1 2 3 4) 0
 -> lindex {1 2 3 4} 0

Blocks are built with the special prefix "begin":

 if ?{> d 0) (begin (puts "Success"))
 -> if {$d > 0} {
    puts "Success"
 }

Comments are allowed but they start with semicolons and end at the end of line.Commands are not required to fit on one line.


package provide lispy 0.4


namespace eval lispy {}

proc lispy::hasescape string {
        expr {[string length $string] - [string length [string trimright $string \\]] % 2 == 1}
}

proc lispy::lexeme code {
        foreach {regex token} {
                {\-?[0-9]+(\.[0-9]*)?([eE][+-]?[0-9]+)?} number
                {".*"} string
                {[a-zA-Z_:][A-Za-z0-9_:]*} var
                {\$[a-zA-Z_:][A-Za-z0-9_:]*} dvar
                {'[a-zA-Z_][A-Za-z0-9_]*} tag
                {[+\-\*/\!=<>%\|&A-Aa-z_:\.][+\-\*/\!=<>%\|&A-Aa-z_:\.0-9]*} cmd
                {'\(} list
                {\?\(} expr
                {\(begin} begin
                {\(} open
                {\)} close
                {;.*\n} comment
                {[\t ]+} blank
        } {
                if {[regexp ^$regex $code match]} {
                                        if {$token eq "string"} {
                                                while {[hasescape [string range $match 0 end-1]]} {
                                                        set rest [string range $code [string length $match] end]
                                                        set i [string first $rest \"]
                                                        if {$i < 0} {error "unfinished string"}
                                                        append match [string range $rest 0 $i]
                                                }
                                                return [list $token $match]
                                        }
                                        if {$token in {close comment blank expr begin open list}} {return [list $token $match]}
                                        if {$code ne $match && [string index $code [string length $match]] ni [split "); \t" ""]} continue                                                
                    return [list $token $match]
                }
        }
        error "invalid syntax at: '$code'"
}

proc lispy::ismath x {
        if {$x in {* + / - % ** < <= == != > >= >> << & && | || ^ ~ ! lt? gt? eq? ne? in? ni? ge? le?}} {
                return yes
        }
        return no
}

proc lispy::ismathfun x {
        set prefix math.
        if {[string range $x 0 4] eq $prefix} {return yes}
        return no
}

proc lispy::math x {
        if {$x in {lt? gt? eq? ne? in? ni? ge? le?}} {return [string range $x 0 1]}
        if {[string range $x 0 4] eq "math."} {return [string range $x 5 end]}
        set x
}

proc lispy::make-parser {name body} {
        proc parse-$name var [string map [list %body [list $body]] {
                upvar 1 code $var
                set res ""
                while {$code ne ""} {
                        foreach {tag string} [lexeme $code] break
                        set code [string range $code [string length $string] end]
                        if {$tag eq "comment"} continue
                        switch -- $tag %body
                }
                return $res
        }]
}

lispy::make-parser command {
        blank - number - string - dvar {
                # put verbatim
                append res $string
                return "$res[parse-arguments code]"
        }
        var - cmd {
                if {[lispy::ismath $string]} {
                                        append res "expr {[join [parse-expr code] [lispy::math $string]]}"
                                        return $res
                                }
                                if {[lispy::ismathfun $string]} {
                                        append res "expr {[lispy::math $string]([join [parse-expr code] ", "])}"
                                        return $res
                                }
                # put verbatim
                append res $string
                return "$res[parse-arguments code]"
        }
        expr {
                        error "expression not allowed as command"
                }
        tag {
                append res [string range $string 1 end]
                return "$res[parse-arguments code]"
        }
        open {
                append res [parse-command code]
                return "$res[parse-arguments code]"
        }
        close {
                return $res
        }
}

# start subcommands for math expressions
# (+ 2 (* 3 4))
#       ^
lispy::make-parser exprcmd {
        blank {}
        string - dvar {
                        append res $string
                        return "\[$res[parse-arguments code]\]"
        }
        var - cmd {
                if {[lispy::ismath $string]} {
                                        return "([join [parse-expr code] [lispy::math $string]])"
                                }
                                if {[lispy::ismathfun $string]} {
                                        return "[lispy::math $string]([join [parse-expr code] ", "])"
                                }
                # put verbatim
                append res $string
                return "\[$res[parse-arguments code]\]"
        }
        tag {
                append res [string range $string 1 end]
                return "\[$res[parse-arguments code]\]"
        }
        open - expr {
                append res [parse-exprcmd code]
                return "\[$res[parse-arguments code]\]"
        }
        close {
                return $res
        }
}

lispy::make-parser block {
        blank {
                append res \n
                continue
        }
        number - string - var - cmd - tag - dvar {
                error "block expected"
        }
        open {
                append res \t[parse-command code]\n
        }
        close {
                return \{$res\}
        }
}
                
lispy::make-parser arguments {
        blank - number - string - cmd - dvar {
                # put verbatim
                append res $string
        }
        tag {
                append res [string range $string 1 end]
        }
        var {
                append res \$$string
        }
        open {
                append res "\[[parse-command code]\]"
        }
        expr {
                                append res "{[parse-exprcmd code]}"
                }
        close {
                return $res
        }
        list {
                append res [parse-list code]
        }
        begin {
                append res [parse-block code]
        }
}
# math expressions
lispy::make-parser expr {
        blank {
                        #ignore
                } 
        number - string - dvar {
                # put verbatim
                lappend res $string
        }
        tag - cmd {
                error "cannot handle tags in a mathematical expression"
        }
        var {
                lappend res \$$string
        }
        open - expr {
                lappend res [parse-exprcmd code]
        }
        close {
                return $res
        }
        list {
                lappend res [parse-list code]
        }
        begin {
                error "cannot have begin in mathematical expression"
        }
}
                
lispy::make-parser list {
        blank - number - string - cmd - var - dvar {
                # put verbatim
                append res $string
        }
        tag {
                append res [string range $string 1 end]
        }
        close {
                return \{$res\}
        }
        list - open {
                append res [parse-list code]
        }
}
                
proc lispy::parse code {
        lispy::parse-command code
}