Version 2 of xsource

Updated 2008-02-11 20:27:53 by sarnold

Sarnold on 2008-02-10: Here is an extended Tcl-syntax preprocessor. It allows the use of expressions anywhere in a Tcl source file.

Usage:

source xsource.tcl
namespace import xsource::*
xsource ?-force? filename

The xsource command creates a new file named 'origname__x.tcl'.

Put the following in a file:

 set x 1
 set y $x+1

And it should translate it into:

 set x 1
 set y [expr {$x+1}]

#===================================================#

#  xsource : an extended syntax parser for Tcl		#

#													#

# Author: Stephane Arnold 2008						#

# License: BSD-style Tcl license					#

#===================================================#



namespace eval xsource {

	namespace export xsource parse

	

	

	proc xsource {args} {

		set syntax "syntax : xsource ?-force? file"

		set fname [lindex $args end]

		set oname ${fname}__x.tcl

		if {[llength $args]==2} {

			assert {[lindex $args 0] eq "-force"} $syntax

			transform $fname $oname

			return [source $oname]

		}

		assert {[llength $args] == 1} $syntax

		if {![file exists $oname] || [file mtime $fname] > [file mtime $oname]} {

			transform $fname $oname

		}

		source $oname

	}

	

	proc transform {in out} {

		assert {![file exists $out] || [file writable $out]} "file $out not writable"

		set r ""

		set fd [open $in]

		while {![eof $fd]} {append r [read $fd]}

		close $fd

		set fd [open $out w]

		puts $fd [parse [string map {\r\n \n} $r]]

		close $fd

	}

	

	proc assert {e {msg {assertion failed}}} {

		if {![uplevel 1 expr [list $e]]} {error $msg}

	}

	# returns yes if index pos of string text is escaped

	proc escaped {text pos} {

		if {$pos == 0} {return no}

		set new [expr {$pos -1}]

		while {[string index $text $new] eq "\\" && $new >0} {

			incr new -1

		}

		expr {($pos-$new)%2==0}

	}

	

	proc flush {vcmd vexpr} {

		upvar 1 $vcmd cmd $vexpr expr

		if {$cmd ne ""} {lappend expr txt $cmd}

		set cmd ""

	}

	

	proc normalize {expr cmd} {

		if {$cmd ne ""} {lappend expr txt $cmd}

		set expr [splitblanks $expr]

		norm2 $expr

	}

	

	proc norm2 {expr} {

		foreach {blanks expr} [shiftBlanks $expr] break

		# returns the string [expr {...}] where ... is the string result of the normalization

		if {[catch {

			foreach {e rest} [_normalize $expr] break

		}]} {return [toValue $expr]}

		if {$e eq ""} {return [toValue $rest]}

		return "$blanks\[expr {$e}\][toValue $rest]"

	}

	

	proc shiftBlanks {expr} {

		set blanks ""

		set i 0

		foreach {type val} $expr {

			if {$type eq "txt" && [string trim $val] eq ""} {

				append blanks $val

				incr i 2

			} else {

				break

			}

		}

		list $blanks [lrange $expr $i end]

	}	

	

	proc toValue {expr} {

		if {![llength $expr]} {return ""}

		set r [getValue [lindex $expr 0] [lindex $expr 1]]

		return $r[norm2 [lrange $expr 2 end]]

	}

	

	proc splitblanks expr {

		set res [list]

		foreach {type val} $expr {

			if {$type eq "txt"} {

				if {[string trim $val] eq ""} {

					lappend res txt $val

				} elseif {![string equal [string trim $val] $val]} {

					set left [string length [string trimleft $val]]

					set right [string length [string trimright $val]]

					set in [string range $val end-[incr left -1] [incr right -1]]

					lappend res txt [string range $val 0 end-[incr left]]

					lappend res txt $in txt [string range $val [incr right] end]

				} else {

					lappend res txt $val

				}

			} else {lappend res $type $val}

		}

		set res

	}

	

	# TODO: parse math expressions

	proc _normalize {expr} {

		# the math parser

		if {![llength $expr]} {return [list "" ""]}

		foreach {type val} $expr {break}

		puts e_,$expr

		if {$type eq "txt"} {

			assert {[string is integer $val] || [string is double $val]}

		}

		foreach {bexpr rest} [getExpr $expr] break

		if {[llength $rest] >= [llength $expr]-2} {return [list "" $expr]}

		list $bexpr $rest

	}

	

	proc in {a l} {expr {[lsearch $l $a]>=0}}

	

	proc isValue {type val} {

		in $type {var txt param}

	}

	

	proc getValue {type val} {

		switch -- $type {

			var {

				return \$$val

			}

			txt - op {

				return $val

			}

			param {

				return \[$val\]

			}

			open - close {return ""}

			default {error "not a value : $type"}

		}

	}

	

	proc getExpr {expr} {

		puts ex,$expr

		set res ""

		set rest ""

		set bexpr [trim $expr]
		if {![llength $bexpr]} {return [list "" $expr]}
		foreach {type val} $bexpr break

		set expr [lrange $bexpr 2 end]

		if {![isValue $type $val]} {

			# unary operator

			if {$type eq "open"} {

				foreach {a expr} [getExpr $expr] break

				append res ($a)

			} elseif {$type eq "close"} {

				return [list "" $expr]

			} {

				assert {$type eq "op"}

				assert {[in $val {- ! ~}]}

				foreach {expr rest} [getExpr $expr] break

				return [list $val$expr $rest]

			}

		}

		append res [getValue $type $val]

		switch -- $type {

			var - txt {

				foreach {type val} [trim $expr] break

				if {$type eq "open"} {

					foreach {a expr} [getExpr [lrange [trim $expr] 2 end]] break

					append res ($a)

				}

			}

		}
		set bexpr [trim $expr]

		if {[llength $bexpr]==0} {return [list $res $expr]}

		if {[lindex $bexpr 0] ne "op"} {

			return [list $res $expr]

		}

		if {[catch {

			foreach {a expr} [getOperator $bexpr] break

			append res $a

		} dummy]} {

			return [list $res $expr]

		}

		foreach {a expr} [getExpr $expr] break

		list $res$a $expr

	}

		

	proc getOperator {expr} {

		foreach {type val} [set expr [trim $expr]] break

		assert {$type eq "op"}

		set res $val

		set bexpr [trim [set expr [lrange $expr 2 end]]]

		if {![llength $bexpr]} {return [list $res $expr]}

		foreach {type val} $bexpr break

		if {$type eq "op"} {

			if {[in $val {- ! ~}]} {return [list "$res $val" [lrange $bexpr 2 end]]}

			assert [in $res$val {== << >> <= >= != && ||}]

			return [list $res$val [lrange $bexpr 2 end]]

		}

		list $res $expr

	}

	

	proc trim {expr} {

		if {[llength $expr]==0} {return $expr}

		foreach {type val} $expr {break}

		if {$type eq "txt" && [string trim $val] eq ""} {

			set expr [trim [lrange $expr 2 end]]

		}

		set expr

	}

		

	# a full Tcl-syntax parser

	proc parse {text {char ""} {up ""}} {

		# go ahead all these CRLF, see later when we parse newlines...

		if {$up ne ""} {upvar i $up} else {set i 0}

		puts t,$text,$i

		# if we have an expression right at the start

		# when we parse "if {$i < 0}" it

		# must NOT be translated to "if {[expr {$i < 0}]}"

		# start is set to TRUE when we are at the beginning of a braced

		# expression before the line ending

		set start [string equal $char \}]

		# init some context variables

		set beforecmd yes

		set result ""

		set inindex [string equal $char )]

		set expr [list]

		set cmd ""; # the current statement

		set inexpr no; # we are not inside a math expression

		set finish no; # a flag to jump to flushing text

		set return no; # a flag to jump to return

		# TODO...

		for {} {$i < [string length $text]} {incr i} {

			set c [string index $text $i]

			switch -- $c {

				\" {
					set beforecmd no

					# inside a string

					if {![escaped $text $i]} {
						set j $i

						incr i
						while {[string index $text $i] ne "\"" && ![escaped $text $i]} {incr i}
						if {$inexpr} {
							flush cmd expr
							lappend expr txt [string range $text $j $i]
							set finish yes
						} else {
							append cmd [string range $text $j $i]
						}
						set c ""

					}

				}

				\{ {
					set beforecmd no

					incr i

					append cmd $c

					append cmd [parse $text \} i]

					set c [string index $text $i]

				}

				\} {

					set return yes

				}

				\[ {
					set beforecmd no

					if {$inexpr} {

						flush cmd expr

						incr i

						lappend expr param [parse $text \] i]
						#incr i
						set c ""

					} elseif {!$inindex && !$start} {
						append result $cmd
						set inexpr yes
						set cmd ""
						incr i
						lappend expr param [parse $text \] i]
						set c ""
					} else {

						incr i

						append cmd $c

						append cmd [parse $text \] i]\]
						set c ""

					}

				}

				\] {

					set return yes

				}

				( {
					set beforecmd no

					# math function call

					if {!$inexpr} {

						incr i

						append cmd ([parse $text ) i]

						incr i

						set c )

					} else {

						flush cmd expr

						lappend expr open ""

					}

				}

				) {

					if {!$inexpr} {

						set return yes

					} else {

						flush cmd expr

						lappend expr close ""

					}

				}
				\# {
					if {$beforecmd} {
						set j $i
						incr i
						set c [string index $text $i]
						while {($c ne "\r" && $c ne "\n")|| [escaped $text $i]} {
							incr i
							set c [string index $text $i]
						}
						if {$inexpr} {
							flush cmd expr
							lappend expr txt [string range $text $j $i]
						} else {
							append cmd [string range $text $j $i]
						}
						set c ""
					}
				}

				\r - \n - ; {

					if {![escaped $text $i]} {

						set finish yes
						set beforecmd yes
						set start no

					}

				}

				\$ {

					if {!$inindex && !$start} {

						set varname [string range $text $i [expr {$i+100}]]

						# it must be a very long variable name to fool me...

						if {[regexp {\{[a-zA-Z0-9_:]+\}} $varname name] || [regexp {[a-zA-Z0-9_:]+} $varname name]} {

							if {$inexpr} {

								flush cmd expr

							} else {

								append result $cmd

								set inexpr yes

								set cmd ""

							}

							lappend expr var $name

							incr i [string length $name]

						}

					}

				}

				+ - \- - / - \* - % - & - | - ^ - < - > - = - ! - , - ~ {

					# an arithmetic operator or somewhat else ressembling

					if {$inexpr} {

						flush cmd expr

						lappend expr op $c

					} elseif {$c eq "-" && !$inindex && !$start} {

						append result $cmd

						set inexpr yes

						set cmd ""

					}

				}

				default {

					if {$inexpr} {

						if {![string match {[0-9a-zA-Z_. \t]} $c]} {

							set finish yes

						}

						if {[string trim $c] eq ""} {flush cmd expr}

						append cmd $c

					} elseif {[string match {[0-9.]} $c] && !$inindex && !$start} {

						# begin expression

						append result $cmd

						set inexpr yes

						set cmd $c

					}

				}

			}

			if {$return && [string equal $c $char]} {

				if {$inexpr} {return $result[normalize $expr $cmd]}

				return $result$cmd

			} else {

				set return no

			}

			if {$inexpr && $finish} {

				puts e,$expr,c,$cmd

				append result [normalize $expr $cmd]

				set inexpr no

				set expr [list]

				set cmd $c

			} elseif {!$inexpr} {

				append cmd $c

			}

			set finish no

		}

		if {$inexpr} {set cmd [normalize $expr $cmd]}

		return $result$cmd

	}

}