Gomoku
Gomoku is a compiler for programs written in the Go Programming
Language, targeting modern-ish C++.
This is an experiment to determine how well Go will perform on embedded
devices. Please see the FAQ for more details on the reasoning behind
this project.
Help to move this forward is greatly appreciated.
Usage
Pass the path to the program being built. Paths relative to $GOHOME
,
relative, and absolute paths are accepted. Output will be generated in
outdir/
under the current directory (this directory will be cleared
every run):
gomoku ./samples/interfaces
This will generate a pair of .cpp
and .h
files for each imported
package (and their dependencies). Generated files are not indented
(yet), so piping them through clang-format
(or other indenting tool)
is recommended.
It is posible that the compiler will abort midway through code
generation since not all data types, statements, and expressions are
implemented yet.
Sample code
surface (from chapter 3 of "The Go Programming Language" by Kernighan and Donovan)
Full code here, excerpt below:
Go
func corner(i, j int) (float64, float64) {
// Find point (x,y) at corner of cell (i,j).
x := xyrange * (float64(i)/cells - 0.5)
y := xyrange * (float64(j)/cells - 0.5)
// Compute surface height z.
z := f(x, y)
// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).
sx := width/2 + (x-y)*cos30*xyscale
sy := height/2 + (x+y)*sin30*xyscale - z*zscale
return sx, sy
}
C++
std::tuple<double, double> corner(int i, int j) {
double sx{0};
double sy{0};
double x{0};
double y{0};
double z{0};
x = xyrange * (double(i) / cells - 0.5);
y = xyrange * (double(j) / cells - 0.5);
z = f(x, y);
sx = width / 2 + (x - y) * cos30 * xyscale;
sy = height / 2 + (x + y) * sin30 * xyscale - z * zscale;
return {sx, sy};
}
samples/interfaces/interfaces.go
Go
type Interfacer interface {
Interface() int
}
type Foo struct {
SomeInt int
}
func (f Foo) Interface() int {
var f2 Foo
return f2.SomeInt * f.ConcreteMethod()
}
func (f Foo) ConcreteMethod() int { return 42 }
func UsingInterfaceType(i Interfacer) int { return i.Interface() * 1234 }
C++
struct Foo : public Interfacer {
int SomeInt{0};
int ConcreteMethod();
virtual int Interface() override;
};
struct Interfacer { // NB: this is declared in the wrong order
virtual int Interface() = 0;
};
int UsingInterfaceType(Interfacer i);
int Foo::Interface() {
Foo f2{};
return f2.SomeInt * this->ConcreteMethod();
}
int Foo::ConcreteMethod() { return 42; }
int UsingInterfaceType(Interfacer i) { return i.Interface() * 1234; }
defer1 (chapter 5 of the same book)
Go
func f(x int) {
fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0
defer fmt.Printf("defer %d\n", x)
f(x - 1)
}
C++
void f(int x) {
moku::defer _defer_;
fmt::Printf("f(%d)\n", x + 0 / x);
_defer_.Push([=]() mutable { fmt::Printf("defer %d\n", x); });
f(x - 1);
}
gcd
Go
func gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}
C++
int gcd(int x, int y) {
while (y != 0) {
std::tie(x, y) = std::tuple<int, int>(y, x % y);
}
return x;
}
Switch statement (tagged)
Go
switch i {
default:
println(4)
fallthrough
case 1:
println(1)
case 2:
println(2)
case 3:
println(3)
}
C++
if ((i == 1)) {
_ident_1_:
println(1);
} else if ((i == 2)) {
_ident_2_:
println(2);
} else if ((i == 3)) {
_ident_3_:
println(3);
} else {
_ident_0_:
println(4);
goto _ident_1_;
}
Switch statement (non-tagged)
Go
switch {
default:
println(0)
case i > 10 && i != 50:
println(1)
case i < 20, i > 150:
println(2)
}
C++
if ((i > 10 && i != 50)) {
_ident_1_:
println(1);
} else if ((i < 20), (i > 150)) {
_ident_2_:
println(2);
} else {
_ident_0_:
println(0);
}
Button and LED sample from Gobot's Getting Started
Go
func main() {
firmataAdaptor := firmata.NewAdaptor("/dev/ttyACM0")
button := gpio.NewButtonDriver(firmataAdaptor, "5")
led := gpio.NewLedDriver(firmataAdaptor, "13")
work := func() {
button.On(gpio.ButtonPush, func(data interface{}) {
led.On()
})
button.On(gpio.ButtonRelease, func(data interface{}) {
led.Off()
})
}
robot := gobot.NewRobot("buttonBot",
[]gobot.Connection{firmataAdaptor},
[]gobot.Device{button, led},
work,
)
robot.Start()
}
C++
void _main() {
ButtonDriver *button{std::nullptr};
Adaptor *firmataAdaptor{std::nullptr};
LedDriver *led{std::nullptr};
Robot *robot{std::nullptr};
std::function<void()> work{std::nullptr};
firmataAdaptor = firmata::NewAdaptor("/dev/ttyACM0");
button = gpio::NewButtonDriver(firmataAdaptor, "5");
led = gpio::NewLedDriver(firmataAdaptor, "13");
work = [=]() mutable -> void {
button->On(gpio::ButtonPush,
[=](moku::empty_interface data) mutable -> void { led->On(); });
button->On(gpio::ButtonRelease,
[=](moku::empty_interface data) mutable -> void { led->Off(); });
};
robot = gobot::NewRobot("buttonBot",
moku::slice<gobot::Connection>{firmataAdaptor},
moku::slice<gobot::Device>{button, led}, work);
robot->Start();
}
FAQ
What's with the name?
Go is one of the oldest board games that are still played today. Gomoku
is a newer game, but is played with the same board and pieces. The rules
are different, but for the untrained eye, they look exactly the same.
The parallels with the Go programming language and this compiler were too
good to not make the pun.
Why not just modify the Go compiler?
There are a few reasons:
- There are many architectures out there that we'd like to support, and
writing a backend for every single one of them would be impractical.
Mainly, we'd like to support x86, ARC, ARM Thumb, Xtensa, and RISC-V.
x86 is already (well) supported by the reference Go compiler, but
microcontroller class x86 will often sport an older ISA (usually i586
class CPU or older), which most likely would involve some maintenance
work. - Go was never designed for embedded applications, specially when
executing on environments with severe memory restrictions such as many
microcontrollers. The reference runtime and compiler assumes this at
every opportunity. - While C++ is a language that people love to hate, the modern variants are
powerful, reasonably expressive, and compilers such as GCC and Clang
leverage years of engineering effort in order to produce correct, efficient
code. All the while supporting all the architectures that we'd want to
support. - The Go compiler was recently changed to use an SSA backend; this made
it easier to write backends, and it's possible to write one that
generates C code. This would mostly take care of the instruction set,
though, still leaving on the table many of the assumptions for the kind
of operating systems the reference compiler has been designed to
generate code for. - This is a good challenge and a great way to learn all the nook and
crannies of a language.
How does transpiling to C++ make Go more "embeddable"? Especially when considering that Go has a non-trivial runtime, including a garbage collector and support for multithreading.
Gomoku isn't targetting 8-bit microcontrollers; it's targetting the
32-bit ones, with reasonably fast CPUs (over 40MHz, sometimes with dual
cores running at over 100MHz), a few hundred kilobytes of RAM, and a
megabyte or so of ROM.
For instance, it's possible to run JavaScript with zephyr.js, which
also has a non-trivial runtime and garbage collector.
Zephyr also has threads and all the basic building blocks necessary to
build Go's primitives on top of, and it's the embedded operating system
Gomoku is targeting first. (Linux will be supported as well mostly for
debugging reasons, of course. Maybe it will be possible to use some Go
libraries from C++ as a "stretch goal".)
The C++ standard template library (stl) will most likely be used at
first to support the Gomoku runtime, but it'll eventually be ditched
for something that works better on more limited platforms. Also, the
Go standard library won't be used as is on embedded devices; a new one
will most likely have to be written. This means that much of what
makes Go the environment that it is won't be available for Gomoku; see
the FAQ on the project name.
What's the license?
It's a 2-clause BSD.
I'd like to contribute. Is there a code of conduct?
Yes. We're using the same code that the Go commmunity
uses.
When it will actually be able to generate compilable code?
It's hard to tell; there are still a lot of things to do. Some of them
are easier than others, but there are a lot of subtle details that are
hard to get right.
But we're very open to contributions, so if you'd like to see this
happen faster, you know what to do.
Will it be self-hosting?
The original Go compiler was written in C, mechanically translated to
Go (with manual work to fix up the translation). If this compiler
could compile itself, it would be almost a full circle. While awesome,
we're quite far away from this possibility.
What platforms will this support?
At first, Linux with STL is going to be the only supported platform.
The reason is that tools such as AddressSanitizer and Valgrind are
invaluable when debugging.
Afterwards, it's very possible that an RTOS such as Zephyr will be
supported. There's a good possibility that STL will be ditched at this
point, favoring either something existing but lightweight, or something
custom made.
In any case, compilation for different operating systems and/or
architectures should be as simple as setting the GOOS and GOARCH
environment variables, or different but related variables that will be
specific to Gomoku.
Are there unit tests?
No, not yet. This is a must and will happen soon.
What about memory management, what's going to be the strategy?
At first, a stop-the-world-mark-and-sweep garbage collector, with
collection happening at allocation phase, when there's no more heap
space available.
This will most likely be custom made, but experimnents with existing
collectors might be performed -- if they're adequate, there will be no need
to write (or modify) one.
Will it use the Go standard library?
At first, yes, as that's what we already have. However, it's very
likely that a lightweight library will be written for embedded devices.
Not everything should be written from scratch, though; some parts from the
standard library might be cherry-picked.
What's necessary to have this actually work?
There are many things that can (and need) to be performed before calling
this even remotely usable. Here's a short list of tasks and things that
need to be implemented; it's by no means complete, and some of these tasks
are way more challenging than others:
- Generating code for imports, not only type information
- Review generation and call of package-level initialization (init func, global calls, etc)
- Ensuring that all types implementing interfaces are assignable to the interface type
- Fix the order of generated types
- Reorganize the package in smaller files
- Implement unit tests
- Get pointer vs. value semantics as correct as possible
- Implement type conversion
- Implement type assertion (incl. type switches)
- Implement basic Go data types (arrays, slices, and maps)
- Nil values
- Comparison with nil values
- Built-in functions (e.g. make(), new(), append(), cap(), etc.)
- Closures / anonymous functions
- Deferred statements
- Range-based loops
- Switch statement
- If statement
- Write a basic standard library for embedded devices
- Memory management with garbage collection
- Perform escape analysis to determine where to allocate things
- Channels (including select statement)
- Goroutines
- Implement _
- Implement built-in functions (println, len, cap, etc.)
- Implement interface expressions for non-empty interfaces