Self-rewriting code

Richard Suchenwirth 2006-04-26 - A question on comp.lang.tcl brought me to experiment with code that rewrites itself. The idea is that settings are not saved in a separate resource file, but in the script itself, as setting a global variable with the desired value. So the next time the script is run, the previously saved setting applies. Here's what I did:

 proc save {filename varlist} {
     set f [open $filename]
     set data [read $f]
     close $f
     set lines {}
     foreach line [split $data \n] {
          foreach var $varlist {
             if [regexp "^ *set $var " $line] {
                 set line [list set $var [set ::$var]]
             }
         }
         lappend lines $line
     }
     set f [open $filename w]
     puts $f [join $lines \n]
     close $f
 }

 #-- Now for a testing demo:
 package require Tk

 #-- We will test with this variable:
 set foo hello

 label  .1 -text foo: 
 entry  .2 -textvar foo  
 button .3 -text Save -command [list save [info script] foo]
 eval pack [winfo children .] -side left 

 #-- Invaluable little helper for rapid restart:
 bind . <Escape> {exec wish $argv0 &; exit}

lexfiend 2006-04-27 - Here's a modification that places all the state variables in a single section, so that [save] doesn't inadvertantly overwrite other [set]s of the state variables and thereby change the program logic. However, note that this forces you to pre-initialize any namespaces on which the state variables depend.

 # Create empty namespaces first
 namespace eval testns {}
 
 ### VARIABLE SAVE SECTION
 set test1 "I am here"
 set testns::test2 "Here as well"
 ### VARIABLE SAVE SECTION
 
 proc save {filename varlist} {
     set f [open $filename]
     set data [read $f]
     close $f
     set lines {}
     set inSaveSection 0
     foreach line [split $data \n] {
         if [regexp "^ *#+ *VARIABLE SAVE SECTION" $line] {
             if {!$inSaveSection} {
                 # Start of save section
                 lappend lines "### VARIABLE SAVE SECTION"
                 foreach var $varlist {
                     lappend lines [list set $var [set ::$var]]
                 }
                 set inSaveSection 1
             } else {
                 # End of save section
                 lappend lines "### VARIABLE SAVE SECTION"
                 set inSaveSection 0
             }
         } elseif {$inSaveSection} {
             # Somewhere in save section - do nothing
         } else {
             lappend lines $line
         }
     }
     set f [open $filename w]
     puts $f [join $lines \n]
     close $f
 }
 
 set testns::test2 "Gone forever"
 set test1 "Not here now"
 save [info script] {test1 testns::test2}

EMJ - 2006-04-26 - Aaaaargh! Self-modifying code, the slippery slope to certain unmaintainability! That of course is a remark from the distant past, when clever people wrote very fast but generally incomprehensible self-modifying assembly language. The warning is still valid, however - be careful!

For the stated purpose, it's not actually too bad, but I think I would use an alias or something rather than just set, in order to make the regex more discriminating. I guess it's simply the reverse of what tkbiff does, writing most of its code to its config file!

IL - This kind of stuff always makes me think of DNA, the assembly of the organic life. I suppose that makes tRNA the compiler and proteins the executable... heh.

Lars H: One remark about the DNA analogy is that biologists nowadays think there is more (perhaps far more) to the genes than just the DNA -- some heritable traits are not even encoded into the nucleotides [L1 ]. (Thinking about it as a programmer, it's easy to suspect that a lot is missing from the highschool biology model of DNA spending all its time being copied to mRNA that is used to build proteins; in all but the very simplest processes, the control and feedback parts are as important as the executive parts, but in this model there aren't any!) From that point of view, the above system is about encoding the entire state into the "DNA" of the program. Apparently that isn't how it's normally done in nature, either.


etdxc - 2006-04-27 - I do something similar. I use a starpack and database (Access in mose cases (ok, everyone groan together!) as a production system. The starpack only contains the libraries, images and the scripts necessary for startup, shutdown, retrieving & storing the data. I retrieve and uplevel the scripts from the database as they are needed. The self modifying ones maintain their data as constants, thus the data becomes the program and vice versa. In three years, I've never had a problem with maintenance because i) of the power of Tcl and ii) I do impact analysis before messing with the code. Of course, all the documentation is in the database seperate but linked to the code. Two of the nicities are i) I never use globals and ii) I can simply store an update as a kit on the web, the program downloads, updates the database and removes it. The updates are able to remove redundant scripts, add functionality, etc. i.e. the original installation script is removed without having any effect on the original program.


Jerry - 2006-04-27 - I posed the question and choose an intermediate to the above two suggestions. I decided to look for variables that were marked by a "volatile" comment:

  set maxIdleTime 1200 ;# volatile

The processing code ( following Richards example ):

 proc save {filename varlist} {
     set f [open $filename]
     set data [read $f]
     close $f
     set lines {}
     foreach line [split $data \n] {
          foreach var $varlist {
             if [regexp  "^ *set $var.*;# volatile" $line] {
                 set line [list set $var [set ::$var] ]
                 append line " ;# volatile"
             }
         }
         lappend lines $line
     }
     set f [open $filename w]
     puts $f [join $lines \n]
     close $f
 }

dcd - 20060427 - strange. I would have chosen "static" rather than "volatile" :-)

escargo - In fact, see also static variables for other self-rewriting code.


Jerry - 2006-04-30 - I found that I had to modify the above code to keep from growing the size of the file. The fix was to replace

 set data [read $filename]

by

 set data [read -nonewline $filename]

The reason why is left as an exercise to the reader :)

lexfiend - I would argue that the correct change is instead from

 puts $f [join $lines \n]

to

 puts -nonewline $f [join $lines \n]

because your change will still grow the file (by 1 or 2 bytes, admittedly) if the original did not end in a newline.


sssbc - 2011-7-7 what about set of array elements - as in "set aray(ndx) foo"? You quickly notice that the any special (to regexp) characters in your variable name cause failure. To be (overly) safe, I escape all special characters:

 proc save {filename varlist} {
     set f [open $filename]
     set data [read $f]
     close $f
     set vslist {}
     foreach var $varlist {
        regsub -all -- {([][$^?+*()|\\])} $var {\\\1} newvar 
        lappend vslist $newvar
     }
     set lines {}
     foreach line [split $data \n] {
          foreach var $varlist vs $vslist {
             if [regexp "^ *set $vs " $line] {
                 set line [list set $var [set ::$var]]
             } 
         }
         lappend lines $line
     }
     set f [open $filename w]
     puts $f [join $lines \n]
     close $f
 }
 
 # now, works to say: save [info script] {test1 testns::test2 aray(ndx)}

Of course, this does not handle "array set aray {ndx foo}" type things, but gotta leave something to be added to this page after another 5 years go by.