Getting Sidetracked (Again)

As in the previous entry, I got sidetracked on the other Inferno-related activity I was writing about: my personal organiser application. The problem I encountered was that exiting the application caused a trap to occur in Inferno, probably related to some aspect of my port to the NanoNote that I hadn't implemented correctly. Nonetheless, I wasn't encouraged to dig into backtraces and MIPS32 machine code to trace the problem.

Not quite a fresh start

Instead, I went back and did what I sometimes do when frustrated with various technologies: try and make something simpler and easier for me to understand. This usually involves thinking about programming languages, compilers and interpreters, experimenting with implementations, rediscovering all the things that need to be done when implementing parsers and code generators, especially all the details that I tend to forget after dealing with them the last time.

The work-in-progress result was that I implemented yet another simple programming language based on ideas from two previous languages: one was a variation on the usual Python-like interpreted language I often return to; the other was an S-expression-based language that was compiled to Thumb-2 code. I was quite happy with the interpreted language but felt that I had made mistakes with the interpreter implementation, discarding a stack-based virtual machine and using a register-based one instead without thinking about some of the implications. The compiler for the compiled language was arguably better implemented, but I just found it awkward to express things in the language syntax.

With these limitations in mind, I took the syntax of the interpreted language and tried to reuse as much of the compiler for the second language as I could. The result is still a mess of sorts, but at least it's a new start and something to work with and build upon. It also considers some of the things that programs need when used on embedded hardware, such as allowing easy definition of static and dynamic data areas.

Making it work

It's all very well designing languages and running tests but the real test is how it all fits together when building something a bit more complex than a simple demo application. Fortunately, the two language implementations I reused had already been exercised to a certain extent, so I wasn't worried about finding too many missing features. As to a real test, I had already decided that my personal organiser application needed reimplementing. This is where the relevance to Inferno comes in because it involves rebuilding the interpreter for the NanoNote using the spim flavour of the Inferno C compiler.

The appearance of the application is a bit more basic than the previous version because I'm not able to use the same set of graphical features that Inferno provides, but it can be improved in time, and the idea was to get something working fairly quickly. The interpreted language takes care of most of the processing, using a C function to copy image data around.


The main menu, calendar and contacts pages

Of course, there were going to be mismatches in the styles of programming used for the Limbo-based application and the new one I was writing. In Limbo I was relying on function pointers to implement a callback mechanism; in my language I rely on a simple inheritance system and run-time method dispatch to give instances of types different behaviour. On the whole, I find the code easier to understand, and I don't find myself fighting the language quite so much. Still, there are things I want to improve with the language itself, and writing this application is a good way to find things to improve. I already have some frustrations with the way parts of the language work, and I feel that some things should be simpler.

There's not much to show in terms of code that would look very different to a snippet of Python source code. The syntax is a bit like Python, but maybe the method syntax is a bit Limbo-like.

def MainMenuPage.keypress(self: MainMenuPage, ch: int) returns bool
    if ch == '\r'
        self.gui.show_page(self.menu.selected + 1)
        return true
    elif ch == KeyEsc
        self.gui.running = false
        return true
    else
        # Send keypresses to widgets.
        return Page.keypress(self, ch)
    end
end

Types can be derived from others and specialised methods can be created to override those from the base type. However, when defining interfaces or collections of related types, we usually need to specify a common base type for compatibility. The compiler will only see that base type when generating a method call because the actual type may only be known at run-time. This is illustrated by the following method, where the page variable holds an instance of the Page type or, more likely, an instance of a Page subtype.

def GUI.show_page(self: GUI, index: int)
    if index >= 0 and index < self.pages.len()
        self.current_page = index
        page = self.pages[index]
        self.surface.draw_rect(0, 0, self.surface.width, self.surface.height, self.bg)
        __sub(page).show()
        self.focus = page
    end
end

The __sub function is a special function that tells to compiler to generate code to resolve the type at run-time and call the appropriate method. All of this extra processing could be avoided if the actual type of the page was used in this method, but that would imply that we would know the type at compile-time, and page can be an instance of any number of Page subtypes.

This resolution of types and the use of dynamic dispatch in addition to static dispatch is something I'm still trying to resolve in my plans for the language. On the one hand, specialising types at compile-time makes it possible to generate efficient code. On the other hand, some features require type resolution to occur at run-time, making aspects of static dispatch redundant.


David Boddie
9 August 2022