Lonami / pyndustric

A Python compiler for Mindustry's logic processors assembly language
MIT License
49 stars 9 forks source link

Support mlog jump statements #7

Closed TelosTelos closed 2 years ago

TelosTelos commented 3 years ago

Short version:

Pyndustric should offer support for using mlog's jump instruction. To a first approximation, this involves offering some sort of syntax for labelling jump destinations, some syntax for commanding a jump to such a labeled destination, and then some means of substituting the right numbers into those jump commands once the line-numbering for the compiled code is established.

Longer justification:

Python doesn't support an arbitrary jump/goto statement, largely because such jumping ability isn't really needed given all the other flexibility that Python has, and because such jumps can be hard to debug and program correctly and can wreak havoc on useful Python features like scopes and namespaces.

However... not all of Python's flexibility is likely to be compilable to mlog -- e.g., we're not likely to get function recursion, class definitions, separate name spaces, or Python generators that can effectively serve as co-routines for flow to jump back and forth between. So we don't have the alternative tools that help to make goto unneeded, nor do we have some of the features that make goto unadvisable. But we do have a jump command that's ready to be used, and often could be used to good effect in relatively simple mindustry logic programs. So it makes sense to provide convenient handles for coders who want to write their mlog programs in Python to be able to use jump, if they want.

(I guess there is a more long-winded way to do this with Pyndustric already, at least when the destination is code that was previously executed, by setting a variable to the @counter when the code is first executed, and then setting the @counter back to that variable when you want to jump back to that previously executed location. But this needlessly wastes an instruction (setting this variable), and won't work for jumping to destinations that won't have already been executed. So better to provide some other way for Python coders to label jump destinations!)

Lonami commented 3 years ago

we're not likely to get function recursion,

We can totally save all function locals to the stack and restore them after the call is complete. This is an initial naive approach, and a step that could be avoided if we can detect there's no recursion in any code path through the call.

class definitions,

A limited form of this, namely dataclasses, would be much easier to implement. The trickiest part is knowing what fields will be needed but a limited subset of dataclasses make this easy.

separate name spaces,

As soon as we start compiling modules, we should get that for free? Ideally we want all variables to be correctly namespaced so that they don't collide (as they do for now in function definitions), and the compiler to be smart enough to see when we refer to outside variables (proper support for global and nonlocal?)

or Python generators that can effectively serve as co-routines for flow to jump back and forth between

This would need similar machinery to function calls, so it shouldn't be impossible?

So we don't have the alternative tools that help to make goto unneeded

if, while and def is pretty much all you need, the rest are nice-to-have. Think Java (which has exceptions and pyndustric doesn't yet, but you don't use those for normal flow, I hope).

Now, for the main issue, perhaps a no-op + function could be added, although I'm not sure we need it:

LABEL = ...
goto(LABEL)
TelosTelos commented 3 years ago

I think you're more optimistic about implementing a working stack and namespaces in mlog than I am. I definitely see how it could be done, but I worry that it starts to become overkill when your processor is operating on the order of 1 kilohertz range, so sure you can get the flexibility of recursive function calls with separate name spaces, but at the cost of a bunch of extra operations for manipulating the stack which would chew up precious processor time. At least for my own immediate interests, I'd like to be able to write code to order units around in a pretty high-level (not nearly so assembly-like) language, but I don't care about building in the full complexity of Python. Similarly, I'd be pretty happy having pyndustric provide the equivalent of classes for commonly used types like blocks, units, and vectors, but wouldn't mind if it doesn't have the flexibility to define new classes (though I agree that a more rigid class definiton with predefined number of attributes would be fairly feasible).

So I'd suggest, at least for the near future, just documenting that there is a single global name space, that calling a function assigns values to the global variables for its arguments (and to a hidden global variable tracking where to return after this function completes), and that recursive function calls could easily generate an infinite loop (since the second call will overwrite the remembered return-to location for the first) unless you take care to manually break out, e.g. with an explicit jump or end command.

Regardless of whether you end up accomplishing some of the greater ambitions, I still think it would be good to include the ability to make use of mlog jump commands with arbitrary destinations, so that Python coders will still have access to this useful functionality. I've tentatively settled upon trying to support this with syntax like the following:


def findPlayer(memory1: MemoryBlock):
    """This compilable function finds the (or a) player and stores a reference to it in slot 1 of memory1.
    This is useful if other logic blocks need to know where the player is, e.g., to direct units to
    follow the player."""
    ubind( gamma )
    if unit: jump("found it")
    ubind( beta )
    if unit: jump("found it")
    ubind( alpha )
    label( "found it" )
    memory1[1] = unit```
Lonami commented 3 years ago

that calling a function assigns values to the global variables for its arguments (and to a hidden global variable tracking where to return after this function completes),

Arguments are currently passed via the stack, and the return address is also put there, so you can definitely call function A from function B without worrying about one overwriting the other. Though using the "function register" to pass these values does sound like a big viable improvement (in terms of code generated) as long as calls are simple.

Lonami commented 3 years ago

Also, your current code could be rewritten as (pseudo):

def findPlayer(memory1: MemoryBlock):
    ubind(gamma)
    if not unit:
        ubind(beta)
        if not unit:
            ubind(alpha)
Lonami commented 2 years ago

Closing, since this is unlikely to be implemented. Modern languages don't offer a GOTO because it hurts readability. It would also feel out of place in Python. And if and while are more than enough.