> Not true. In imperative languages, the stack is a part of the state, and the state is a part of the denotation.

By that definition then you could call any operational semantics "denotational" and the distinction between the two would be pointless.

> The functional denotational semantics contribute, IMO, to the least tricky parts of a program

Largely agreed (though it depends on your domain; it's very possible to have genuinely tricky business logic in the pure parts of your code, or indeed to have some genuinely tricky code that you only ever runs once to output a computed result) but in a well-factored functional-style program you can move the vast majority of program code/logic into those "least tricky parts", and have a machine-enforced separation between the two. So you can restrict the more expensive forms of analysis to a small subset of your code, and get a much better cost/benefit.

> it still cannot be done to relate fac1 to fac5, even though they are the exact same algorithm.

If you deliberately choose to use operations that are deliberately defined to not have a formal denotation then your code won't have a formal denotation. Complaining about that is like complaining that if you write a cast in your code then you can violate type safety. IO is explicitly an "escape hatch" to be used as a last resort.

If you use a monad that has semantics, or you choose to model (some subset of) IO, you can relate them. E.g. if your model of IO follows our common-sense informal semantics (where we e.g. pretend concurrent access to the same file wouldn't happen - this is a choice you have to make when modelling, because in some contexts the ability of another process to interfere with fac.txt would be an important difference between fac1 and fac5 and we wouldn't actually want them to be equivalent), (writeFile x y *> readFile x >>= (evaluate . force) >>= (writeFile x) . f) = writeFile x (f y) would be an equation in that model. Using that and using read . show = id (and show 1 = "1"), we can reduce fac5 equationally to writeFile "fac.txt" . show . fac1 (or to a normal form of fac1 if we want a full reduction, but we don't need to). The whole exercise is little different from reducing an arithmetic expression to 4 as you did in another reply.

> Well, the denotational semantics for imperative languages is very easily applied to IO, and is beautifully and elegantly done in a language like Eve.

So terminology aside, what's the problem? Use operational semantics to reason about IO, which you can do just as well in a Haskell-like language as in a non-Haskell-like language (I find the synchronous logic approach horribly confusing, but if you like it I see no reason you couldn't apply it to Haskell IO. Making the modelling of IO easier is not really a priority for me, because I spend so little of my time reasoning about IO, but it's not like I'm opposed). Use denotational semantics to reason about non-IO - which can be done much more cheaply because the space of possibilities to consider is so much smaller.

> most modern programs are interactive/concurrent/distributed, and so their trickiest bits require IO and nondeterminism in general.

Not my experience - the only cases I've seen where the tricky logic couldn't be unpicked from the IO and nondeterminism, the entire code was incomprehensible (not just to me but to everyone who tried). And conversely, doing that unpicking has always made the logic easier for a human reader to follow.