Hi everyone,

It seems this hasn't garnered much attention.

Nevertheless, meanwhile I've realised the code had a few bugs, in
particular around the backwards-compatibility flag.

I've done a major rewrite:

   - fixed couple of bugs I found in the original implementation
   - made implementation more robust, simpler, and backwards-compatible:
   - added new expect-chars macro to support the new
      non-backwards-compatible matcher procedure signature.
      - removed need for expect-pass-char? backwards-compatibility feature
      flag. expect and expect-strings macros work as in ice-9.
      - improved interact macro, to allow specifying separate expect/input
   and interact/output ports.

The code can be found here
<https://github.com/dadinn/common/blob/feature/expect4/expect.scm>.

Thanks,
Daniel


On Sat, 15 Apr 2023 at 16:10, Daniel Dinnyes <dinny...@gmail.com> wrote:

> Hi everyone,
>
> I am new to this mailing list, and writing this because I have encountered
> some issues with ice-9 expect
> <https://git.savannah.gnu.org/cgit/guile.git/tree/module/ice-9/expect.scm>
> :
>
> The procedural expect macro has a serious problem:
>
> This syntax expects a procedure as the test expression, with 2 arguments
> (actual content read, and boolean EOF flag). The primary problem is that
> when the test expression is not a identifier bound to such a procedure, but
> instead an expression which creates such a procedure (aka "factory"), then
> the expression will be evaluated anew for each character read from
> expect-port. This is a serious problem: when constructing that procedure
> has significant computational costs, or side effects (eg. opening a port to
> a new log file), as it can be seen in my code here
> <https://github.com/dadinn/system-setup-tests/blob/expect-bug/run.scm#L476>.
> (This is a branch of some of my KVM-based E2E testing code, which uses the
> original expect macro implementation, just to demonstrate the problem).
>
> I had written a workaround for this problem, by binding the evaluation of
> the test expression outside the expansion of the expect macro (as it can be
> seen here
> <https://github.com/dadinn/system-setup-tests/blob/master/run.scm#L29>).
> The problem with this was, that it doesn't support the full capabilities of
> the original expect macro, and debilitates it by only allowing for a single
> conditional branch. Also, I haven't verified, but I suspicious it would
> possibly even break the behaviour for the => syntax of the expect-strings
> macro.
>
> To implement a more complete version of this (and also as an exercise to
> understand hygienic macros), I've attempted to rewrite my workaround using
> syntax-case. I've ran into some issues, and my implementation didn't
> expand as I expected. After some discussion on IRC, we've concluded
> <http://snailgeist.com/irc-logs/?message_id=136306> that this was because
> the current (ice-9 expect) implementation is written using unhygienic
> defmacro syntax, which doesn't work well together with the hygienic
> syntax-case. It was suggested I should rather rewrite expect completely
> to be hygienic instead.
>
> I've eventually completed the rewrite using syntax-case here
> <https://github.com/dadinn/common/blob/feature/expect/expect.scm>, and
> would be happy to contribute it back to the ice-9 module!
> I would point out the following about this implementation:
>
>    1. It works!!! I had some quite extensive E2E testing code written
>    before here
>    <https://github.com/dadinn/system-setup-tests/blob/develop/run.scm>,
>    which I think really stretches the possibilities with (ice-9 expect).
>    Switching to the new implementation was simple, and works flawlessly!
>    2. It is backwards compatible! I've gone extra lengths to make sure
>    the code would be a backwards compatible, a drop-in replacement.
>    3. Introduced new feature for the procedural expect macro, such that
>    the second argument of the procedure gets passed the currently read char,
>    or #f if it was an EOF character. There is multiple reasons this is
>    superior to passing just a boolean eof? flag argument:
>    1. the same logic can be implemented as with the eof? boolean, just by
>          negating the condition
>          2. passing the currently read char avoids having to do the
>          inefficient operation to retrieve the currently read char by cutting 
> off
>          the last character from the end of the content. Currently it is 
> necessary
>          to do this, if the logic wants something additional to be done with 
> the
>          current char (eg. as it can be seen in my code here
>          
> <https://github.com/dadinn/system-setup-tests/blob/develop/run.scm#L200>
>          )
>          3. one such case of additional use is making the expect-char-proc
>          property obsolete, as the predicate procedure is now able to execute 
> such
>          logic too, besides other more advanced logging scenarios (like it 
> can be
>          seen in my code here
>          
> <https://github.com/dadinn/system-setup-tests/blob/develop/run.scm#L205>:
>          here besides logging everything to STDOUT, and also to a main 
> log-file, I
>          also log for each expect macro call the contents it tries to compare
>          against into a separately named minor log-file (useful for 
> debugging), and
>          also to ensure that we actually do not write to STDOUT when using 
> expect on
>          multiple parallel processes)
>          4. To ensure that everything stays backwards compatible, this
>          new feature is only enabled if the new expect-pass-char? binding
>          is set to #t in the lexical context (like here
>          
> <https://github.com/dadinn/system-setup-tests/commit/21bddce3cd1c14f8834adaee4ff75f5b1b7a7b04>).
>          Otherwise, by default this feature is disabled, and boolean eof?
>          flag argument is passed just like before.
>          4. There is a better support for using default value/expressions
>    for the various parameters the macros depend on in their lexical contexts.
>    The old macro had to export default values defined in its module, so as to
>    ensure the defaults are available. This either clutters the module's global
>    context where (ice-9 expect) is used, or the defaults are not
>    available when using something like ((ice-9 expect) #:select (expect
>    expect-string)).  The new defaults are calculated based on the lexical
>    context where the macros are used, and expands into the correct default
>    values when they are not defined. For example when expect-port is not
>    defined in the lexical context, then the default value used is going to be
>    the result of the (current-input-port) expression. I would like
>    someone to review whether I've implemented this correctly, as it seems the
>    current-input-port identifier default for expect-port gets captured in
>    the expansion. Not sure why that happens.
>    5. The expect-strings macro supports the => syntax, which passes the
>    output(s) of the test predicate to a procedure, instead of just evaluating
>    a body. This is fully supported, but additionally, the => syntax now
>    also works with the procedural expect macro too! I haven't verified if the
>    original implementation did this too, but at least it wasn't described in
>    the documentation!
>    6. As an added bonus, I have added a simplistic implementation for an
>    interact macro. It uses threads to read from STDIO, and expects expect-port
>    to be an input-output-port, so that it can output the lines read from STDIO
>    into the same port. Not too advanced, as it doesn't support a full terminal
>    interface, with autocomplete, etc... but works well enough for my use-case
>    to interact with a KVM process via serial port.
>    7. mnieper said <http://snailgeist.com/irc-logs/?message_id=136211>
>    that I am not using syntax-case idiomatically, and my style is more in the
>    form of a syntax-rules-based state-machine.
>    I am quite happy with how it is currently, but would be open to be
>    convinced if there is a superior way of expressing all this.
>    8. I currently have various tests sprinkled around my implementation,
>    to manually demonstrate expansions, and executions.
>    Also, I have added some automated unit tests for the bind-locally
>    helper procedure, which I used for checking whether parameters are defined
>    in the lexical scope.
>    9. I am not happy with how I am managing my project structure, and how
>    my modules can reference each other (having to call add-to-load-path
>    everywhere).
>    Also, I think the way I am using srfi-64 tests is a bit bothersome, as
>    they always get executed when my modules are loaded, and I have to globally
>    disable logging to files.
>    I would like some recommendations how to do this better!
>
> If the community likes this implementation, I would be happy to
> apply/rebase my changes onto the official repo, and then we can go from
> there!
>
> Best regards,
> Daniel Dinnyes
>
>
>

Reply via email to