On Tue, May 13, 2014 at 7:34 PM, Steven D'Aprano <st...@pearwood.info> wrote:
> On Tue, 13 May 2014 15:56:50 +1000, Chris Angelico wrote:
>
>> On Tue, May 13, 2014 at 3:48 PM, Steven D'Aprano <st...@pearwood.info>
>> wrote:
>>> Self-modifying code is a nightmare inside the head of a Lovecraftian
>>> horror. There's a reason why almost the only people still using self-
>>> modifying code are virus writers, and the viruses they create are
>>> notorious for being buggy.
>>
>> Hmm... what counts as self-modifying, though?
>
> Code that modifies itself, as opposed to code that modifies other code.

Duh :)

>> When you decorate a
>> function to modify its behaviour (eg add caching around it), all the
>> modification happens at compile time,
>
> Not in Python it doesn't. Functions are created at runtime -- def is a
> statement, and it runs at runtime. But that's actually irrelevant.

Yes, but that's still the "initialization phase" of the code. When I
think of self-modifying code, I think of something that could do this:

def some_func(x):
    if x<0:
        change_bottom_of_function
        x = -x
    result = calculate_stuff(x)
    return result # becomes "return -result" if change done

Effectively, instead of using a stack or heap entry to record state,
it uses its own code. (And the above example is buggy, too. That's
part of why this is a bad idea - it's not obvious when there are
bugs.)

With a decorated function, the change happens at the time the def is
executed. With the above example, the change happens when the function
is called. That's what I meant by "compile time", although I did pick
a poor term for it. Is there a better word for that? There's three
phases, if you like - compilation, execution of def, and execution of
function.

>> but it's still executing something
>> other than what you see as the bald code.
>
> I don't think so. ... there's no *self-modification* going on.
> You can write obfuscated code with decorators, but you can
> shoot yourself in the foot with just about any tool. Fundamentally,
> decorators are more or less just a way of doing function composition.

Right. I would agree here. It's confusing if it completely changes
itself, but it's not really self-modifying.

>> What about a microkernel that
>> can load altered code from the disk, compile it into memory, and replace
>> portions of itself?
>
> Now we're talking self-modification.

Technically not - that's why I said microkernel. The kernel never
changes *itself*, it just loads other code into memory (and can switch
out submodules). It's not so clear-cut. The program, as a whole, is
changing its own behaviour, but by separating "this bit can change"
from "this bit manages the changes", you tame the self-modification -
just as you say further down.

> But of course there are degrees of
> self-modification. This isn't too bad, because it's relatively simple to
> follow what happens next:
>
> def foo(x):
>     global foo  # Change me.
>     foo = new_foo
>     return bar(x)
>
> This is to self-modifying code what break and continue are to GOTO --
> tamed, trained, on a leash, and pretty safe.

Yeah. It's safe because it's atomic and clean.

>> Or a JIT compiler. As you run something, it gets
>> changed in form to be more streamlined.
>
> I wouldn't call that self-modifying code, because the compiler isn't
> modifying itself. It's modifying the code being run.

Again, technically. But if the compiler is deemed to be part of the
program (it often won't be - V8 with your JavaScript code and PyPy
with your Python code are separate - but it would be possible to
consider them to be the same effective unit), then it's exactly the
same as the microkernel example above: tamed self-modification, with
one piece modifying the other(s).

> But note that JIT compilers are optimizing compilers (there's little
> point, otherwise), and highly optimized code is notorious for being hard
> to debug and containing subtle, hard to find, harder to fix, bugs. Why
> are they hard to debug? Because the code that you read is not necessarily
> the same as the code being run.

Right, and that's exactly where the risk is. Every translation between
you and what's run is a potential source of horribly insidious bugs.
That's why it's absolutely essential to put bounds on the insatiable
ambition for automutability. In the microkernel case, and also with a
similar (but horribly less efficient) structure that I did a couple of
times with DLLs, there's a clear protocol with "this file provides
these public symbols", and that protocol doesn't much change with
reloads. With a JIT compiler, the code should have the exact same
effect before and after the change, and just be faster. And so on. By
clearly delineating what can change and what can't, you divide any
debugging job down to smaller pieces.

Actually, even the file system can do some of this to you. I was
checking lsof on one of my Linux systems a little while ago, and found
that I had half a dozen old versions of a program, all with the same
file name, all deleted. (When you unlink a running binary on Linux,
the program keeps running the old version, and then you can put a new
version in its place, with the same file name. Any new invocation will
pick up the new binary; existing ones keep what they have.) So a
program can unlink itself, write out a new version of itself, and
happily keep going - letting new invocations take the changed version,
safely. Again, not exactly self-modifying code... or is it? Again,
tamed by boundaries.

ChrisA
-- 
https://mail.python.org/mailman/listinfo/python-list

Reply via email to