diff options
Diffstat (limited to 'tcllib/modules/wip')
-rw-r--r-- | tcllib/modules/wip/ChangeLog | 100 | ||||
-rw-r--r-- | tcllib/modules/wip/pkgIndex.tcl | 5 | ||||
-rw-r--r-- | tcllib/modules/wip/wip.man | 384 | ||||
-rw-r--r-- | tcllib/modules/wip/wip.tcl | 463 | ||||
-rw-r--r-- | tcllib/modules/wip/wip2.tcl | 464 |
5 files changed, 1416 insertions, 0 deletions
diff --git a/tcllib/modules/wip/ChangeLog b/tcllib/modules/wip/ChangeLog new file mode 100644 index 0000000..4449185 --- /dev/null +++ b/tcllib/modules/wip/ChangeLog @@ -0,0 +1,100 @@ +2013-02-01 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.15 ======================== + * + +2011-12-13 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.14 ======================== + * + +2011-01-24 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.13 ======================== + * + +2010-04-07 Andreas Kupries <andreask@activestate.com> + + * wip2.tcl (run_next_*): Fixed the run_next_ commands to stop + * wip.tcl: when detecting the end of the program. New feature, + * wip.man: a callback to handle unknown command words, defaults + * pkgIndex.tcl: to throwing an error. Updated documentation. + Bumped versions to 1.2 and 2.2. + +2010-04-05 Andreas Kupries <andreask@activestate.com> + + * wip2.tcl (run_next_if, run_nextifnot): Extended API, two + * wip.tcl (run_next_if, run_nextifnot): more run_next_ + * wip.man: commands to run a single following command instead + * pkgIndex.tcl: of all acceptable. Updated documentation. Bumped + versions to 1.1.3 and 2.1.3. + +2009-12-07 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.12 ======================== + * + +2009-10-27 Andreas Kupries <andreask@activestate.com> + + * wip.tcl: Made sure that wip classes are in the global + * wip2.tcl: namespace. + +2009-03-02 Andreas Kupries <andreask@activestate.com> + + * wip.man: Made the use of 'wip' class in the snit macro + * wip.tcl: 'wip::dsl' fully qualified to prevent mis-resolutions + * wip2.tcl: of the name in case the user is a '...::wip' class + * pkgIndex.tcl: itself. In snit say this would resolve to the user + instead of the wip interpreter. Bumped the versions to 1.1.2 and + 2.1.2 respectively. + +2008-12-12 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.11.1 ======================== + * + +2008-10-16 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.11 ======================== + * + +2008-09-03 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * wip.tcl: Changed the name of the wip processor component added + * wip2.tcl: to DSL system, prevent it from clashing with the name + * pkgIndex.tcl: of the wip-core snit::type. Bumped the versions to + * wip.man: 1.1.1 and 2.1.1. + +2007-10-26 Andreas Kupries <andreask@activestate.com> + + * wip.tcl: Extended error reporting, and fixes of bad indices in + * wip2.tcl: the methods manipulating the program (insert, push, + etc). wip v2 only extended error reporting. + + * wip.tcl: Extended with method to undef DSL commands. + * wip2.tcl: Updated both variants, and the documentation. + * wip.man: Bumped versions to 1.1 and 2.1 respectively. + * pkgIndex.tcl: + +2007-09-12 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.10 ======================== + * + +2007-07-30 Andreas Kupries <andreask@activestate.com> + + * wip.man: Fixed bugs uncovered during test of + * wip.tcl: first user, fileutil::multi::op. + * wip2.tcl: + +2007-07-27 Andreas Kupries <andreask@activestate.com> + + * New Module. + diff --git a/tcllib/modules/wip/pkgIndex.tcl b/tcllib/modules/wip/pkgIndex.tcl new file mode 100644 index 0000000..7968ea1 --- /dev/null +++ b/tcllib/modules/wip/pkgIndex.tcl @@ -0,0 +1,5 @@ +if {![package vsatisfies [package provide Tcl] 8.4]} {return} +package ifneeded wip 1.2 [list source [file join $dir wip.tcl]] + +if {![package vsatisfies [package provide Tcl] 8.5]} {return} +package ifneeded wip 2.2 [list source [file join $dir wip2.tcl]] diff --git a/tcllib/modules/wip/wip.man b/tcllib/modules/wip/wip.man new file mode 100644 index 0000000..bfffab3 --- /dev/null +++ b/tcllib/modules/wip/wip.man @@ -0,0 +1,384 @@ +[comment {-*- text -*-}] +[manpage_begin wip n 2.2] +[keywords interpreter] +[keywords list] +[keywords word] +[copyright {2007-2010 Andreas Kupries <andreas_kupries@users.sourceforge.net>}] +[moddesc {Word Interpreter}] +[titledesc {Word Interpreter}] +[category {Programming tools}] +[require Tcl 8.4] +[require wip [opt 2.2]] +[require snit [opt 1.3]] +[require struct::set] +[description] +[para] + +This package provides a micro interpreter for lists of words. Domain +specific languages based on this will have a bit of a Forth feel, with +the input stream segmented into words and any other structuring left +to whatever the language desired. Note that we have here in essence +only the core dispatch loop, and no actual commands whatsoever, making +this definitely only a Forth feel and not an actual Forth. + +[para] + +The idea is derived from Colin McCormack's [package treeql] processor, +modified to require less boiler plate within the command +implementations, at the expense of, likely, execution speed. In +addition the interface between processor core and commands is more +complex too. + +[section {GENERAL BEHAVIOUR}] + +Word interpreters have a mappping from the names of the language +commands they shall recognize to the methods in the engine to invoke +for them, and possibly fixed arguments for these methods. This mapping +is largely static, however it is possible to change it during the +execution of a word list (= program). + +[para] + +At the time a language command is defined the word interpreter will +use [package snit]'s introspection capabilities to determine the +number of arguments expected by the method of the egnine, and together +with the number of fixed arguments supplied in the method prefix of +the mapping it then knows how many arguments the language command is +expecting. This is the command's [term arity]. Variable-argument +methods (i.e. with the last argument named [arg args]) are [emph not] +allowed and will cause the word interpreter to throw an error at +definition time. + +[para] + +Note that while I said [package snit]'s abilities the engine object +can be written in any way, as long as it understands the method +[method {info args}], which takes a method name and returns the list +of arguments for that method. + +[para] + +When executing a list of words (aka program) the first word is always +taken as the name of a language command, and the next words as its +arguments, per the [term arity] of the command. Command and argument +words are removed from the list and then associated method of the +engine is executed with the argument words. The process then repeats +using the then-first word of the list. + +[para] + +Note that the methods implementing the language commands may have full +access to the list of words and are allowed to manipulate as they see +fit. + +[list_begin enum] + +[enum] +This means, for example, that while we cannot specify +variable-argument methods directly they can consume words after their +fixed arguments before returning to the execution loop. This may be +under the control of their fixed arguments. + +[enum] +Another possibility is the use of method [method run_next] and its +variants to execute commands coming after the current command, +changing the order of execution. + +[enum] +Execution can be further changed by use of the program accessor +methods which allow a command implementation to modify the remaining +list of words (insert, replace, prepend, append words) without +executing them immediately. + +[enum] +At last the basic [method run] methods save and restore an existing +list of words when used, enabling recursive use from within command +implementations. + +[list_end] + +[section {CLASS API}] + +The main command of the package is: + +[list_begin definitions] + +[call [cmd ::wip] [arg wipName] [arg engine] [arg arg]...] + +The command creates a new word interpreter object with an associated +global Tcl command whose name is [arg wipName]. If however the string +[const %AUTO%] was used as object name the package will generate its +own unique name for the object. + +[para] + +The [arg engine] is the object the word interpreter will dispatch all +recognized commands to, and the [arg arg]uments are a word list which +defines an initial mapping from language words to engine methods. + +[para] + +The recognized language of this word list is + +[list_begin definitions] +[call [cmd def] [arg name]] + +Defines [arg name] as command of the language, to be mapped to a +method of the [arg engine] having the same name. + +[call [cmd def] [arg name] [arg method_prefix]] + +Defines [arg name] as command of the language, to be mapped to the +method of the [arg engine] named in the [arg method_prefix]. + +[list_end] + +[para] + +The returned command may be used to invoke various operations on the +object. It has the following general form: + +[list_begin definitions] +[call [cmd wipName] [arg option] [opt [arg "arg arg ..."]]] + +[arg Option] and the [arg arg]s determine the exact behavior of the +command. + +[list_end] +[list_end] + +The package additionally exports the command: + +[list_begin definitions] + +[call [cmd wip::dsl] [opt [arg suffix]]] + +This command is for use within snit types which wish to use one or +more wip interpreters as a component. Use within the type definition +installs most of the boilerplate needed to setup and use a word +interpreter. + +[para] + +It installs a component named [emph wip], and a method +[method wip_setup] for initializing it. This method has to be called +from within the constructor of the type using the word interpreter. + +If further installs a series of procedures which make the object API +of the word interpreter directly available to the type's methods, +without having to specify the component. + +[para] + +[emph Note] that this does and cannot install the language to +interpret, i.e. the mapping from words to engine methods. + +[para] + +It is possible to instantiate multiple word interpreter components +within a type by using different suffices as arguments to the command. + +In that case the name of the component changes to +'wip_[var \$suffix]', the setup command becomes +'wip_[var \$suffix]_setup' and all the procedures also get the suffix +'_[var \$suffix]'. + +[list_end] + +[section {OBJECT API}] + +The following commands are possible for word interpreter objects: + +[list_begin definitions] + +[call [arg wipName] [method def] [arg name] [opt [arg method_prefix]]] + +Defines a language command [arg name] and maps it to the method named +in the engine's [arg method_prefix]. If the [arg method_prefix] name +is not specified it is simply the name of the language command. + +[call [arg wipName] [method defl] [arg names]] + +Defines a series of language commands, specified through the list of +[arg names], all of which are mapped to engine methods of the same +name. + +[call [arg wipName] [method defd] [arg dict]] + +Defines a series of language commands, specified through the +dictionary [arg dict] of names and method prefixes. + +[call [arg wipName] [method deflva] [arg name]...] + +As method [method defl], however the list of names is specified +through multiple arguments. + +[call [arg wipName] [method defdva] ([arg name] [arg method_prefix])...] + +As method [method defd], however the dictionary of names and method +prefixes is specified through multiple arguments. + +[call [arg wipName] [method undefl] [arg names]] + +Removes the named series of language commands from the mapping. + +[call [arg wipName] [method undefva] [arg name]...] + +As method [method undefl], however the list of names is specified +through multiple arguments. + +[call [arg wipName] [method unknown] [arg cmdprefix]] + +Sets the handler for unknown words to [arg cmdprefix]. This command +prefix takes one argument, the current word, and either throws some +error, or returns the result of executing the word, as defined by the +handler. The default handler simply throws an error. + +[call [arg wipName] [method runl] [arg wordlist]] + +Treats the list of words in [arg wordlist] as a program and executes +the contained command one by one. The result of the command executed +last is returned as the result of this command. + +[para] + +The [arg wordlist] is stored in the object for access by the other +[term run]-methods, and the general program accessor methods (see +below). A previously stored wordlist is saved during the execution of +this method and restored before it returns. This enables the recursive +execution of word lists within word lists. + +[call [arg wipName] [method run] [arg word]...] + +As method [method runl], however the list of words to execute is +specified through multiple arguments. + +[call [arg wipName] [method run_next]] + +Low-level method. Determines the next word in the list of words, and +its arguments, and then executes it. The result of the executed word +is the result of this method. + +[para] + +Exposed for use within command implementations. + +The methods [method run] and [method runl] use it to execute words +until their word list is exhausted. + +[call [arg wipName] [method run_next_while] [arg acceptable]] + +Low-level method. Invokes the method [method run_next] as long as the +next word is in the set of [arg acceptable] words, and the program is +not empty. The result of the command executed last is returned as the +result of this command. + +[para] + +Exposed for use within command implementations to change the order of +execution. + +[call [arg wipName] [method run_next_until] [arg rejected]] + +Low-level method. Invokes the method [method run_next] until the next +word is in the set of [arg rejected] words, and the program is not +empty. The result of the command executed last is returned as the +result of this command. + +[para] + +Exposed for use within command implementations to change the order of +execution. + +[call [arg wipName] [method run_next_if] [arg acceptable]] + +Low-level method. Invokes the method [method run_next] if the next +word is in the set of [arg acceptable] words, and the program is not +empty. The result of the command executed last is returned as the +result of this command. + +[para] + +Exposed for use within command implementations to change the order of +execution. + +[call [arg wipName] [method run_next_ifnot] [arg rejected]] + +Low-level method. Invokes the method [method run_next] if the next +word is not in the set of [arg rejected] words, and the program is not +empty. The result of the command executed last is returned as the +result of this command. + +[para] + +Exposed for use within command implementations to change the order of +execution. + +[call [arg wipName] [method next]] + +Returns the next word in the programm. The word is also removed. + +[call [arg wipName] [method peek]] + +Returns the next word in the programm without removing it + +[call [arg wipName] [method peekall]] + +Returns the remaining programm in toto. + +[call [arg wipName] [method insertl] [arg at] [arg wordlist]] + +Basic programm accessor method. Inserts the specified [arg wordlist] +into the program, just before the word at position [arg at]. Positions +are counted from [const zero]. + +[call [arg wipName] [method replacel] [arg wordlist]] + +Basic programm accessor method. Replaces the whole stored program with +the specified [arg wordlist]. + +[call [arg wipName] [method pushl] [arg wordlist]] + +Program accessor method. The specified [arg wordlist] is added to the +front of the remaining program. Equivalent to + +[para] +[example {$wip insertl 0 $wordlist}] + +[call [arg wipName] [method addl] [arg wordlist]] + +Program accessor method. The specified [arg wordlist] is appended at +the end of the remaining program. Equivalent to + +[para] +[example {$wip insertl end $wordlist}] + +[call [arg wipName] [method insert] [arg at] [arg word]...] + +Like method [method insertl], except the words are specified through +multiple arguments. + +[call [arg wipName] [method replace] [arg word]...] + +Like method [method setl], except the words are specified through +multiple arguments. + +[call [arg wipName] [method push] [arg word]...] + +Like method [method pushl], except the words are specified through +multiple arguments. + +[call [arg wipName] [method add] [arg word]...] + +Like method [method addl], except the words are specified through +multiple arguments. + +[list_end] + +[section EXAMPLES] + +No examples yet. + +[vset CATEGORY wip] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/wip/wip.tcl b/tcllib/modules/wip/wip.tcl new file mode 100644 index 0000000..881a6a4 --- /dev/null +++ b/tcllib/modules/wip/wip.tcl @@ -0,0 +1,463 @@ +# ### ### ### ######### ######### ######### +## +# (c) 2008-2009 Andreas Kupries. + +# WIP = Word Interpreter (Also a Work In Progress :). Especially while +# it is running :P + +# Micro interpreter for lists of words. Domain specific languages +# based on this will have a bit of a Forth feel, with the input stream +# segmented into words and any other structuring left to whatever +# language. Note that we have here in essence only the core dispatch +# loop, and no actual commands whatsoever, making this definitely only +# a Forth feel and not an actual Forth. + +# The idea is derived from Colin McCormack's treeql processor, +# modified to require less boiler plate within the command +# implementations, at the expense of, likely, execution speed. In +# addition the interface between processor core and commands is more +# complex too. + +# ### ### ### ######### ######### ######### +## Requisites + +package require Tcl 8.4 + +# For Tcl 8.{3,4} only snit1 of a suitable patchlevel is possible. +package require snit 1.3 + +# The run_next_* methods use set operations (x in set) +package require struct::set + +# ### ### ### ######### ######### ######### +## API & Implementation + +snit::type ::wip { + + # ### ### ### ######### ######### ######### + ## API + + constructor {e args} {} ; # create processor + + # Defining commands and where they dispatch to. + method def {name {cp {}}} {} ; # Define a DSL command. + method def/ {name arity {cp {}}} {} ; # Ditto, with explicit arity. + method defl {names} {} ; # Def many, simple names (cp = name) + method defd {dict} {} ; # s.a. name/cp dict + method deflva {args} {} ; # s.a. defl, var arg form + method defdva {args} {} ; # s.a. defd, var arg form + + method undefva {args} {} ; # Remove DSL commands from the map. + method undefl {names} {} ; # Ditto, names given as list. + + # Execution of word lists. + method runl {alist} {} ; # execute list of words + method run {args} {} ; # ditto, words as varargs + method run_next {} {} ; # run the next command in the input. + method run_next_while {accept} {} ; # s.a., while acceptable command + method run_next_until {reject} {} ; # s.a., until rejectable command + method run_next_if {accept} {} ; # s.a., if acceptable command + method run_next_ifnot {reject} {} ; # s.a., if not rejectable command + + # Manipulation of the input word list. + method peek {} {} ; # peek at next word in input + method next {} {} ; # pull next word from input + method insert {at args} {} ; # insert words back into the input + method push {args} {} ; # ditto, at == 0 + + # ### ### ### ######### ######### ######### + ## Processor construction. + + constructor {e args} { + if {$e eq ""} { + return -code error "No engine specified" + } + set engine $e + $self unknown [mymethod ErrorForUnknown] + $self Definitions $args + return + } + + method Definitions {alist} { + # args = series of 'def name' and 'def name cp' statements. + # The code to handle them is in essence a WIP too, just + # hardcoded, as state machine. + + set state expect-def + set n {} + set cp {} + foreach a $alist { + if {$state eq "expect-def"} { + if {$a ne "def"} { + return -code error "Expected \"def\", got \"$a\"" + } + set state get-name + } elseif {$state eq "get-name"} { + set name $a + set state get-cp-or-def + } elseif {$state eq "get-cp-or-def"} { + # This means that 'def' cannot be a command prefix for + # DSL command. + if {$a eq "def"} { + # Short definition, name only, completed. + $self def $name + # We already have the first word of the next + # definition here, name is coming up next. + set state get-name + } else { + # Long definition, name + cp, completed. + $self def $name $a + # Must be followed by the next definition. + set state expect-def + } + } + } + if {$state eq "get-cp-or-def"} { + # Had a short definition last, now complete. + $self def $name + } elseif {$state eq "get-name"} { + # Incomplete definition at the end, bogus + return -code error "Incomplete definition at end, name missing." + } + return + } + + # ### ### ### ######### ######### ######### + ## Processor state + ## Handle of the object incoming commands are dispatched to. + ## The currently active DSL code, i.e. word list. + + variable unknown {} ; # command prefix invoked when + # encountering unknown command words. + variable engine {} ; # command + variable program {} ; # list (string) + variable arity -array {} ; # array (command name -> command arity) + variable cmd -array {} ; # array (command name -> method cmd prefix) + + # ### ### ### ######### ######### ######### + ## API: DSL definition + + ## DSL words map to method-prefixes, i.e. method names + fixed + ## arguments. We store them with the engine already added in front + ## to make them regular command prefixes. No 'mymethod' however, + ## that works only in engine code itself, not form the outside. + + method def {name {mp {}}} { + if {$mp eq {}} { + # Derive method-prefix from DSL word. + set mp [list $name] + set m $name + set n 0 + + } else { + # No need to check for an empty method-prefix. That cannot + # happen, as it is diverted, see above. + + set m [lindex $mp 0] + set n [expr {[llength $mp]-1}] + } + + # Get method arguments, check for problems. + set a [$engine info args $m] + if {[lindex $a end] eq "args"} { + return -code error "Unable to handle Tcl varargs" + } + + # The arity of the command is number of required arguments, + # with compensation for those already covered by the + # method-prefix. + + set cmd($name) [linsert $mp 0 $engine] + set arity($name) [expr {[llength $a] - $n}] + return + } + + method def/ {name ay {mp {}}} { + # Like def, except that the arity is specified + # explicitly. This is for methods with a variable number of + # arguments in their definition, possibly dependent on the + # fixed parts of the prefix. + + if {$mp eq {}} { + # Derive method-prefix from DSL word. + set mp [list $name] + set m $name + + } else { + # No need to check for an empty method-prefix. That cannot + # happen, as it is diverted, see above. + + set m [lindex $mp 0] + } + + # The arity of the command is specified by the caller. + + set cmd($name) [linsert $mp 0 $engine] + set arity($name) $ay + return + } + + method deflva {args} { $self defl $args ; return } + method defdva {args} { $self defd $args ; return } + method defl {names} { foreach n $names { $self def $n } ; return } + method defd {dict} { + if {[llength $dict]%2==1} { + return -code error "Expected a dictionary, got \"$dict\"" + } + foreach {name mp} $dict { + $self def $name $mp + } + return + } + + method undefva {args} { $self undefl $args ; return } + method undefl {names} { + foreach name $names { + unset -nocomplain cmd($name) + unset -nocomplain arity($name) + } + return + } + + # ### ### ### ######### ######### ######### + ## API: DSL execution + # + ## Consider moving the core implementation into procs, to reduce + ## call overhead + + method run {args} { + return [$self runl $args] + } + + method runl {alist} { + # Note: We are saving the current program and restore it + # afterwards, this handles the possibility that this is a + # recursive call into the dispatcher. + set saved $program + set program $alist + set r {} + while {[llength $program]} { + set r [$self run_next] + } + set program $saved + return $r + } + + method run_next_while {accept} { + set r {} + while {[llength $program] && [struct::set contains $accept [$self peek]]} { + set r [$self run_next] + } + return $r + } + + method run_next_until {reject} { + set r {} + while {[llength $program] && ![struct::set contains $reject [$self peek]]} { + set r [$self run_next] + } + return $r + } + + method run_next_if {accept} { + set r {} + if {[llength $program] && [struct::set contains $accept [$self peek]]} { + set r [$self run_next] + } + return $r + } + + method run_next_ifnot {reject} { + set r {} + if {[llength $program] && ![struct::set contains $reject [$self peek]]} { + set r [$self run_next] + } + return $r + } + + method run_next {} { + # The first word in the list is the current command. Determine + # the number of its fixed arguments. This also checks command + # validity in general. + + set c [lindex $program 0] + if {![info exists arity($c)]} { + # Invoke the unknown handler + return [uplevel #0 [linsert $unknown end $c]] + } + + set n $arity($c) + set m $cmd($c) + + # Take the fixed arguments from the input as well. + + if {[llength $program] <= $n} { + return -code error -errorcode WIP \ + "Not enough arguments for command \"$c\"" + } + + set cargs [lrange $program 1 $n] + incr n + + # Remove the command to dispatch, and its fixed arguments from + # the program. This is done before the dispatch so that the + # command has access to the true current state of the input. + + set program [lrange $program $n end] + + # Now run the command with its arguments. Commands needing + # more than the declared fixed number of arguments are + # responsible for reading them from input via the method + # 'next' provided by the processor core. + + # Note: m already has the engine at the front, it was stored + # that way, see 'def'. + + if {![llength $cargs]} { + return [eval $m] + } else { + # Explanation: First linsert constructs 'linsert $m end {*}$cargs', + # which the inner eval transforms into '{*}$m {*}$cargs', which at + # last is run by the outer eval. + return [eval [eval [linsert $cargs 0 linsert $m end]]] + } + } + + # ### ### ### ######### ######### ######### + ## Input manipulation + + # Get next word from the input (shift) + method next {} { + set w [lindex $program 0] + set program [lrange $program 1 end] + return $w + } + + # Peek at the next word in the input + method peek {} { + return [lindex $program 0] + } + + # Retrieve the whole current program + method peekall {} { + return $program + } + + # Replace the current programm + method replace {args} { + set program $args + return + } + method replacel {alist} { + set program $alist + return + } + + # Insert words into the input stream. + method insert {at args} { + set program [eval [linsert $args 0 linsert $program $at]] + return + } + method insertl {at alist} { + set program [eval [linsert $alist 0 linsert $program $at]] + return + } + + # <=> insert 0 + method push {args} { + set program [eval [linsert $args 0 linsert $program 0]] + return + } + method pushl {alist} { + set program [eval [linsert $alist 0 linsert $program 0]] + return + } + + # <=> insert end + method add {args} { + set program [eval [linsert $args 0 linsert $program end]] + return + } + method addl {alist} { + set program [eval [linsert $alist 0 linsert $program end]] + return + } + + # ### ### ### ######### ######### ######### + + method unknown {cmdprefix} { + set unknown $cmdprefix + return + } + + method ErrorForUnknown {word} { + return -code error -errorcode WIP \ + "Unknown command \"$word\"" + } + + ## + # ### ### ### ######### ######### ######### +} + +# ### ### ### ######### ######### ######### +## + +# Macro to declare the method of a component as proc. We use this +# later to make access to a WIP processor simpler (no need to write +# the component reference on our own). And no, this is not the same as +# the standard delegation. Doing that simply replaces the component +# name in the call with '$self'. We remove the need to have this +# written in the call. + +snit::macro wip::methodasproc {var method suffix} { + proc $method$suffix {args} [string map [list @v@ $var @m@ $method] { + upvar 1 {@v@} dst + return [eval [linsert $args 0 $dst {@m@}]] + }] +} + +# ### ### ### ######### ######### ######### +## Ready + +# ### ### ### ######### ######### ######### +## + +# Macro to install most of the boilerplate needed to setup and use a +# WIP. The only thing left is to call the method 'wip_setup' in the +# constructor of the class using WIP. This macro allows the creation +# of multiple wip's, through custom suffices. + +snit::macro wip::dsl {{suffix {}}} { + if {$suffix ne ""} {set suffix _$suffix} + + # Instance state, wip processor used to run the language + component mywip$suffix + + # Standard method to create the processor component. The user has + # to manually add a call of this method to the constructor. + + method wip${suffix}_setup {} [string map [list @@ $suffix] { + install {mywip@@} using ::wip "${selfns}::mywip@@" $self + }] + + # Procedures for easy access to the processor methods, without + # having to use self and wip. I.e. special delegation. + + foreach {p} { + add addl def undefva undefl + defd defdva defl deflva def/ + insert insertl replace replacel + push pushl run runl + next peek peekall run_next + run_next_until run_next_while + run_next_ifnot run_next_if + } { + wip::methodasproc mywip$suffix $p $suffix + } + return +} + +# ### ### ### ######### ######### ######### +## Ready + +package provide wip 1.2 diff --git a/tcllib/modules/wip/wip2.tcl b/tcllib/modules/wip/wip2.tcl new file mode 100644 index 0000000..9cd5a19 --- /dev/null +++ b/tcllib/modules/wip/wip2.tcl @@ -0,0 +1,464 @@ +# ### ### ### ######### ######### ######### +## +# (c) 2008-2010 Andreas Kupries. + +# WIP = Word Interpreter (Also a Work In Progress :). Especially while +# it is running :P + +# Micro interpreter for lists of words. Domain specific languages +# based on this will have a bit of a Forth feel, with the input stream +# segmented into words and any other structuring left to whatever +# language. Note that we have here in essence only the core dispatch +# loop, and no actual commands whatsoever, making this definitely only +# a Forth feel and not an actual Forth. + +# The idea is derived from Colin McCormack's treeql processor, +# modified to require less boiler plate within the command +# implementations, at the expense of, likely, execution speed. In +# addition the interface between processor core and commands is more +# complex too. + +# ### ### ### ######### ######### ######### +## Requisites + +package require Tcl 8.5 + +# Use new Tcl 8.5a6+ features for specification of allowed packages. +# We can use snit 1.3 and anything above (incl. v2+). +package require snit 1.3- + +# The run_next_* methods use set operations (x in set) +package require struct::set + +# For 8.5 we are using features like word-expansion to simplify the +# various evaluations. Otherwise this is identical to v1. + +# ### ### ### ######### ######### ######### +## API & Implementation + +snit::type ::wip { + + # ### ### ### ######### ######### ######### + ## API + + constructor {e args} {} ; # create processor + + # Defining commands and where they dispatch to. + method def {name {cp {}}} {} ; # Define a DSL command. + method def/ {name arity {cp {}}} {} ; # Ditto, with explicit arity. + method defl {names} {} ; # Def many, simple names (cp = name) + method defd {dict} {} ; # s.a. name/cp dict + method deflva {args} {} ; # s.a. defl, var arg form + method defdva {args} {} ; # s.a. defd, var arg form + + method undefva {args} {} ; # Remove DSL commands from the map. + method undefl {names} {} ; # Ditto, names given as list. + + # Execution of word lists. + method runl {alist} {} ; # execute list of words + method run {args} {} ; # ditto, words as varargs + method run_next {} {} ; # run the next command in the input. + method run_next_while {accept} {} ; # s.a., while acceptable command + method run_next_until {reject} {} ; # s.a., until rejectable command + method run_next_if {accept} {} ; # s.a., if acceptable command + method run_next_ifnot {reject} {} ; # s.a., if not rejectable command + + # Manipulation of the input word list. + method peek {} {} ; # peek at next word in input + method next {} {} ; # pull next word from input + method insert {at args} {} ; # insert words back into the input + method push {args} {} ; # ditto, at == 0 + + # Set callback for unknown command words. + method unknown {commandprefix} {} + + # ### ### ### ######### ######### ######### + ## Processor construction. + + constructor {e args} { + if {$e eq ""} { + return -code error "No engine specified" + } + set engine $e + $self unknown [mymethod ErrorForUnknown] + $self Definitions $args + return + } + + method Definitions {alist} { + # args = series of 'def name' and 'def name cp' statements. + # The code to handle them is in essence a WIP too, just + # hardcoded, as state machine. + + set state expect-def + set n {} + set cp {} + foreach a $alist { + if {$state eq "expect-def"} { + if {$a ne "def"} { + return -code error "Expected \"def\", got \"$a\"" + } + set state get-name + } elseif {$state eq "get-name"} { + set name $a + set state get-cp-or-def + } elseif {$state eq "get-cp-or-def"} { + # This means that 'def' cannot be a command prefix for + # DSL command. + if {$a eq "def"} { + # Short definition, name only, completed. + $self def $name + # We already have the first word of the next + # definition here, name is coming up next. + set state get-name + } else { + # Long definition, name + cp, completed. + $self def $name $a + # Must be followed by the next definition. + set state expect-def + } + } + } + if {$state eq "get-cp-or-def"} { + # Had a short definition last, now complete. + $self def $name + } elseif {$state eq "get-name"} { + # Incomplete definition at the end, bogus + return -code error "Incomplete definition at end, name missing." + } + return + } + + # ### ### ### ######### ######### ######### + ## Processor state + ## Handle of the object incoming commands are dispatched to. + ## The currently active DSL code, i.e. word list. + + variable unknown {} ; # command prefix invoked when + # encountering unknown command words. + variable engine {} ; # command + variable program {} ; # list (string) + variable arity -array {} ; # array (command name -> command arity) + variable cmd -array {} ; # array (command name -> method cmd prefix) + + # ### ### ### ######### ######### ######### + ## API: DSL definition + + ## DSL words map to method-prefixes, i.e. method names + fixed + ## arguments. We store them with the engine already added in front + ## to make them regular command prefixes. No 'mymethod' however, + ## that works only in engine code itself, not from the outside. + + method def {name {mp {}}} { + if {$mp eq {}} { + # Derive method-prefix from DSL word. + set mp [list $name] + set m $name + set n 0 + + } else { + # No need to check for an empty method-prefix. That cannot + # happen, as it is diverted, see above. + + set m [lindex $mp 0] + set n [expr {[llength $mp]-1}] + } + + # Get method arguments, check for problems. + set a [$engine info args $m] + if {[lindex $a end] eq "args"} { + return -code error "Unable to handle Tcl varargs" + } + + # The arity of the command is the number of required + # arguments, with compensation for those already covered by + # the method-prefix. + + set cmd($name) [linsert $mp 0 $engine] + set arity($name) [expr {[llength $a] - $n}] + return + } + + method def/ {name ay {mp {}}} { + # Like def, except that the arity is specified + # explicitly. This is for methods with a variable number of + # arguments in their definition, possibly dependent on the + # fixed parts of the prefix. + + if {$mp eq {}} { + # Derive method-prefix from DSL word. + set mp [list $name] + set m $name + + } else { + # No need to check for an empty method-prefix. That cannot + # happen, as it is diverted, see above. + + set m [lindex $mp 0] + } + + # The arity of the command is specified by the caller. + + set cmd($name) [linsert $mp 0 $engine] + set arity($name) $ay + return + } + + method deflva {args} { $self defl $args ; return } + method defdva {args} { $self defd $args ; return } + method defl {names} { foreach n $names { $self def $n } ; return } + method defd {dict} { + if {[llength $dict]%2==1} { + return -code error "Expected a dictionary, got \"$dict\"" + } + foreach {name mp} $dict { + $self def $name $mp + } + return + } + + method undefva {args} { $self undefl $args ; return } + method undefl {names} { + foreach name $names { + unset -nocomplain cmd($name) + unset -nocomplain arity($name) + } + return + } + + # ### ### ### ######### ######### ######### + ## API: DSL execution + # + ## Consider moving the core implementation into procs, to reduce + ## call overhead + + method run {args} { + return [$self runl $args] + } + + method runl {alist} { + # Note: We are saving the current program and restore it + # afterwards, this handles the possibility that this is a + # recursive call into the dispatcher. + set saved $program + set program $alist + set r {} + while {[llength $program]} { + set r [$self run_next] + } + set program $saved + return $r + } + + method run_next_while {accept} { + set r {} + while {[llength $program] && [struct::set contains $accept [$self peek]]} { + set r [$self run_next] + } + return $r + } + + method run_next_until {reject} { + set r {} + while {[llength $program] && ![struct::set contains $reject [$self peek]]} { + set r [$self run_next] + } + return $r + } + + method run_next_if {accept} { + set r {} + if {[llength $program] && [struct::set contains $accept [$self peek]]} { + set r [$self run_next] + } + return $r + } + + method run_next_ifnot {reject} { + set r {} + if {[llength $program] && ![struct::set contains $reject [$self peek]]} { + set r [$self run_next] + } + return $r + } + + method run_next {} { + # The first word in the list is the current command. Determine + # the number of its fixed arguments. This also checks command + # validity in general. + + set c [lindex $program 0] + if {![info exists arity($c)]} { + # Invoke the unknown handler + set program [lrange $program 1 end] + return [uplevel #0 [list {*}$unknown $c]] + } + + set n $arity($c) + set m $cmd($c) + + # Take the fixed arguments from the input as well. + + if {[llength $program] <= $n} { + return -code error -errorcode WIP \ + "Not enough arguments for command \"$c\"" + } + + set cargs [lrange $program 1 $n] + incr n + + # Remove the command to dispatch, and its fixed arguments from + # the program. This is done before the dispatch so that the + # command has access to the true current state of the input. + + set program [lrange $program $n end] + + # Now run the command with its arguments. Commands needing + # more than the declared fixed number of arguments are + # responsible for reading them from input via the method + # 'next' provided by the processor core. + + # Note: m already has the engine at the front, it was stored + # that way, see 'def'. + + return [{*}$m {*}$cargs] + } + + # ### ### ### ######### ######### ######### + ## Input manipulation + + # Get next word from the input (shift) + method next {} { + set w [lindex $program 0] + set program [lrange $program 1 end] + return $w + } + + # Peek at the next word in the input + method peek {} { + return [lindex $program 0] + } + + # Retrieve the whole current program + method peekall {} { + return $program + } + + # Replace the current programm + method replace {args} { + set program $args + return + } + method replacel {alist} { + set program $alist + return + } + + # Insert words into the input stream. + method insert {at args} { + set program [linsert $program $at {*}$args] + return + } + method insertl {at alist} { + set program [linsert $program $at {*}$alist] + return + } + + # <=> insert 0 + method push {args} { + set program [linsert $program 0 {*}$args] + return + } + method pushl {alist} { + set program [linsert $program 0 {*}$alist] + return + } + + # <=> insert end + method add {args} { + set program [linsert $program end {*}$args] + return + } + method addl {alist} { + set program [linsert $program end {*}$alist] + return + } + + # ### ### ### ######### ######### ######### + + method unknown {cmdprefix} { + set unknown $cmdprefix + return + } + + method ErrorForUnknown {word} { + return -code error -errorcode WIP \ + "Unknown command \"$word\"" + } + + ## + # ### ### ### ######### ######### ######### +} + +# ### ### ### ######### ######### ######### +## + +# Macro to declare the method of a component as proc. We use this +# later to make access to a WIP processor simpler (no need to write +# the component reference on our own). And no, this is not the same as +# the standard delegation. Doing that simply replaces the component +# name in the call with '$self'. We remove the need to have this +# written in the call. + +snit::macro wip::methodasproc {var method suffix} { + proc $method$suffix {args} [string map [list @v@ $var @m@ $method] { + upvar 1 {@v@} dst + return [$dst {@m@} {*}$args] + }] +} + +# ### ### ### ######### ######### ######### +## Ready + +# ### ### ### ######### ######### ######### +## + +# Macro to install most of the boilerplate needed to setup and use a +# WIP. The only thing left is to call the method 'wip_setup' in the +# constructor of the class using WIP. This macro allows the creation +# of multiple wip's, through custom suffices. + +snit::macro wip::dsl {{suffix {}}} { + if {$suffix ne ""} {set suffix _$suffix} + + # Instance state, wip processor used to run the language + component mywip$suffix + + # Standard method to create the processor component. The user has + # to manually add a call of this method to the constructor. + + method wip${suffix}_setup {} [string map [list @@ $suffix] { + install {mywip@@} using ::wip "${selfns}::mywip@@" $self + }] + + # Procedures for easy access to the processor methods, without + # having to use self and wip. I.e. special delegation. + + foreach {p} { + add addl def undefva undefl + defd defdva defl deflva def/ + insert insertl replace replacel + push pushl run runl + next peek peekall run_next + run_next_until run_next_while + run_next_ifnot run_next_if + } { + wip::methodasproc mywip$suffix $p $suffix + } + return +} + +# ### ### ### ######### ######### ######### +## Ready + +package provide wip 2.2 |