I sort-of more-or-less have something more appealing w.r.t. compile-time/hygienic matching, but very non-portable:
First off, this version makes all state-variable names are declared - they cannot be made-up on the fly at runtime.
1) the state object is just an eq-hashtable. A fresh state object is always empty. Default values for state-variables are inserted as needed.
2) the implementation must provide a function (identifier->eqobj #'id) which returns a unique key associated with the identifier's binding (or non-binding). Two identifiers produce the same eq? key IFF then they match hygienically, and are free-identifier=?. These keys are the runtime keys used to store values in the state. All state-variables are bound.
3) The original define-environment-monad form still works, but there is also (declare-fields <monad-name> <fieldspec> ...), where a field spec is either (fieldname) or (fieldname default). This is how an extension module can declare more state variables (and not worry about symbolic collisions).
4) (fn (col) ...) does not bind 'col'. col, and all declared state variables are initially defined as syntax-parameters. (fn (col) ...) binds a temporary identifier and then parameterizes col to be identifier-syntax for the temporary.
5) (fn ((current-column col)) ...) does the simpler thing, just binds current-column
6) fn, with!, and with forms expand to a syntax-error if a state-variable name is not a declared state variable associated with the same monad that defined these syntax. (This was ugly to implement, but it definitely helped with debugging).
So, the key non-portables here are identifier->eqobj and identifier-syntax. Also to call identifier->eqobj with an identifier as an argument from syntax-rules macro output, (syntax id) -- aka #' -- is needed.
No need to pick this one apart. It's not production-grade for sure. Just though it might be interesting, and I've love to hear what others are thinking on this topic.