Mascot based on the Go gopher by Renée French, licensed CC BY 4.0.
A modern, zero-allocation, Go-like programming language built specifically for the Parallax Propeller 2 (P2) multi-core microcontroller.
OctoGo brings the elegance of Go's concurrency model to embedded hardware. It compiles an LL(1) subset of a Go-like language into C99/C11, which is then compiled to native P2 assembly using the industry-standard flexprop toolchain.
There is no software scheduler and no garbage collector. Goroutines map 1:1 to physical silicon.
The repository you are currently viewing might be a mirror. Please review the guidelines below based on where you are viewing this:
| Platform | Role | Contributing Guidelines |
|---|---|---|
| GitLab | Primary Source | This is the canonical repository (cznic/ogo), where development happens. |
| GitHub | Mirror | This is a mirror (modernc-org/ogo). We do accept Issues and Pull Requests here for your convenience! Note: PRs submitted here will be manually merged into the GitLab source, so please allow extra time for processing. |
Forget manual state machines and timer interrupts. In OctoGo, spawning a parallel hardware process is as simple as calling go myfunc(). Channel communication uses the familiar <- operator, allowing you to synchronize hardware locks seamlessly.
import "p2"
// blinkWorker runs independently on its own dedicated Cog
func blinkWorker(pin int, rateChan chan int) {
delay := 500
for {
// Non-blocking hardware poll via select
select {
case delay = <-rateChan:
default:
}
p2.PinHigh(pin)
p2.WaitMs(delay)
p2.PinLow(pin)
p2.WaitMs(delay)
}
}
func main() {
// A channel is made as Go makes one. Its rendezvous cell is a static or a
// frame's, not a heap's, and main's outlives every cog.
rateChan := make(chan int)
// Spawns directly to a new hardware Cog!
go blinkWorker(56, rateChan)
// Update the blink rate from the main Cog via hardware-locked channel
rateChan <- 100
}
go install modernc.org/ogo@latest
Building ogo needs Go 1.26 or newer. It is built and tested on linux/amd64,
linux/arm64, windows/amd64, darwin/arm64 and darwin/amd64 — if you
need another platform, please say so, it helps decide which to do first. Note that the two
halves come apart: ogo build emits an ordinary P2 .binary, so you can compile
on one machine and flash with whatever loader you already have on another.
On Windows, run
ogofromcmd.exeor PowerShell — not a Unix-emulation shell (git-bash, MSYS2, Cygwin). The board-facing commands (run,loadp2) drive the serial port through the native Windows console and are unreliable under those shells: the P2 handshake times out intermittently and the terminal's exit key can stop responding (use Ctrl-C to escape). Building (build,fmt) is unaffected and works in any shell.
That is the whole toolchain. ogo embeds the C backend (flexspin's compiler,
transpiled to Go) and the P2 loader, so there is no flexprop installation, no
separate loader and no SDK path to configure.
ogo <command> [arguments]
The commands are:
| Command | |
|---|---|
build |
compile packages and dependencies |
fmt |
reformat source files |
help |
show help for a command |
loadp2 |
load a program onto a Propeller 2 board (loadp2 passthrough) |
run |
compile and run a program on a connected board |
smith |
output a random program for compiler testing |
test |
test packages |
version |
print the ogo version |
Run ogo help for this list at the terminal, or ogo help <command> — for
example ogo help build — for one command's flags and detail. A typical session:
- Write your
.ogocode. - Run
ogo build blinky/to compile and generate your P2 binary. - Run
ogo run blinky/to compile it, load it onto a connected board and open a terminal. - Run
ogo test blinky/to run that package's_test.ogotests, on the board.
Tests run on the board and nowhere else. A test is func TestSomething(t *testing.T), and testing is imported by name with nothing on disk:
import "testing" func TestPop(t *testing.T) { if got, ok := pop(); !ok || got != 3 { t.Errorf("pop: got %d, %v; want 3, true", got, ok) } }
t.Log, Logf, Error, Errorf, Fatal, Fatalf, Skip, Skipf, Fail,
FailNow, SkipNow, Name and Helper are Go's, and the output reads as
go test -v's does, each message after its file and line. A format is a constant
string, as the builtin printf's is, since nothing is formatted into memory.
FailNow and its kin stop the test where they are called, however deep, but run no
deferred call on the way out, as a panic runs none here; and a message is placed on
the line it is written on, Helper or not. A t.Log can't be deferred or started
with go directly yet; wrap it in a function literal. Nor is it a value yet, f := t.Logf, and no interface holds a *testing.T or a *testing.B yet, any among
them: a helper takes a *testing.T parameter. ogo test ./... tests
every package under a root, one board run each; ogo test -run Push runs only the
tests whose name matches, and compiles only those, which is what makes iterating on
one of them worth the board's ten seconds. ogo test -c builds
the tests without running them, for a machine with no board attached.
A benchmark, func BenchmarkSum(b *testing.B), runs where ogo test -bench . asks,
after the tests, as go test runs one: b.N grown until a run takes -benchtime,
a second by default or a count written 100x, and the result printed as
go test -v -bench prints it, its time measured by the P2's 64-bit system counter
at the clock the cpu: label names. b.ResetTimer, StopTimer, StartTimer,
SetBytes, ReportAllocs (no allocation is ever reported) and ReportMetric are
Go's, and so are b.Loop, the loop Go 1.24's benchmarks are written with, and
sub-benchmarks, b.Run; RunParallel is not provided yet. A function literal
capturing a variable is refused here as everywhere, so a table-driven benchmark
hands each case to its sub-benchmark through a package variable.
ogo build --report says what a binary takes of the P2 once it is built: Hub RAM
by its parts and what main's stack needs beside them, each goroutine's stack and
the go statement it is for, the hardware locks the runtime takes, the program's
data by package variable, each with what its value is made of (a slice's backing,
a channel's cell and buffer), and its largest functions by their Go names. Every
size is the C backend's own, read off its listing and its binary. For p2-11:
Hub RAM: 357544 of 524288 bytes in the binary; main's stack needs 2572 more, 164176 to spare
header 1028 the loader's and the clock's
cog runtime 1364 the code each cog loads into its registers
code 79872 219 functions as the backend laid them out, the largest below
...
Goroutines: 4 go statements, each run once, on a slot of its own
longs needs go statement
512 509 disk.ogo:113 turn
160 156 main.ogo:137 receive
...
Data: the largest of the program's 269260 bytes
253952 memory [126976]uint16
4816 machine pdp11.Machine
4100 the goroutine slots: stacks, arguments, bookkeeping
...
Code: the largest of 219 functions
3992 (*fat.Volume).Mount
2924 (*rk11.Controller).begin
...
Seeing garbled serial output?
ogo runreads at 230400 baud, which is what a compiled program writes at, soprintlnoutput is readable out of the box. The rawogo loadp2passthrough uses loadp2's own defaults instead, and it reads at 115200 — a baud mismatch, so the output arrives as rubbish bytes rather than text. Pass the matching baud and nothing else is needed:ogo loadp2 -b 230400 -t prog.binaryThe
-ffrequency is not part of the fix for a program built here: such a program sets its own clock as it starts, so-fdecides neither what it runs at nor what its output looks like. The clock is a build-time choice —ogo build --clock 200MHz— and 160 MHz is the default.
_examples/vacuum is a vacuum system's PLC, after the one Rayman wrote in Spin2
for a P2 and posted on the Parallax forum: a scan loop on the main cog that reads
the inputs, drives the pumps and valves and runs the front panel through two I2C
expanders, a Modbus RTU server on a cog of its own, the vacuum gauges' serial lines
parsed on another, interlocks, a safe state when the panel stops answering, and
the status screen of a 1.8" LCD. The hardware is modelled, so it imports nothing
and is held to Go's output on the host and on a P2-EDGE, as life is.
_examples/spin2drivers calls the two Spin2 drivers that PLC runs on, Jon
McPhalen's jm_i2c and the eight-port serial driver, as they are, from two OctoGo
packages of bodyless functions.
ogo run _examples/vacuum ogo run _examples/spin2drivers
_examples/gopher puts the Go gopher on a scope in X/Y mode: two smart pins run as
DACs, one X and one Y, and the beam walks the outline while persistence does the
rest. There is no framebuffer and no display controller — the figure is the two
voltages, redrawn about a thousand times a second. Eight frames make it dance.
ogo run _examples/gopher # then probe the two pins it names, scope in X/Y mode_examples/life runs Conway's Game of Life on the board and imports nothing at
all — no pin, no cog, no intrinsic, just arrays, structs, methods and loops. Give
the source a package clause and spell printf fmt.Printf and it compiles as Go,
which is not a claim it is left to make: the test suite performs exactly those two
substitutions, runs the twin under go run, runs this one on a real P2-EDGE, and
requires the two to print the same bytes.
ogo run _examples/life
That is a stronger check than a test corpus gets. Pinning behaviour against what somebody wrote down as expected is only as good as the person who wrote it; pinning it against the language OctoGo is modelled on, running the same source, is not.
An OctoGo program is written as Go is written, so Go's own tools are a good place to learn the language and to try a program before it goes to the board. Visual Studio Code with the Go extension gives completion, errors as you type, go to definition, formatting and a debugger. On Windows, roughly (elsewhere only the installer differs):
- Install Go from https://go.dev/dl/ (on Windows, the
.msi). - Install VS Code, and from its Extensions view the Go extension by the Go Team at Google. When it offers to install its tools (gopls, dlv), let it.
- Make a folder, open it in VS Code, and in its terminal (View > Terminal) run
go mod init helloonce. Go builds a program as a module, and without thego.modthis writes,go run .and F5 stop with "go.mod file not found in current directory or any parent directory".go env -w GO111MODULE=automakes them work too, by turning on the GOPATH mode that predates modules;go mod initis the current way, andgo env -u GO111MODULEundoes the setting. - Create
main.gowithpackage mainand afunc main, and run it withgo run .or debug it with F5.
An example that imports no p2 runs this way too, which is how _examples/vacuum
can be stepped through in the debugger: copy its main.ogo into such a folder as
main.go, put package main and import "fmt" at its top and var printf = fmt.Printf at its end, and Go prints what the P2 prints — the test suite holds the
two to the same bytes. Then ogo run the original on the board. An example that
imports p2 drives pins and cogs and runs only on a P2. For the language itself,
the Tour of Go runs in the browser.
The language is specified in the package documentation: OctoGo Language Specification. It is a draft and is reconciled with the implementation as the compiler moves; the Status section below is the shorter answer to what works today, and CHANGELOG.md records what changed in each release — including the cases where a new release rejects a program the last one accepted. DIFFERENCES.md lists what builds both as OctoGo and as Go and runs differently — a panic and its deferred calls, a recursion at the end of its stack, among others — each with a program and what each compiler makes of it, run as a test on a P2 board and under Go.
OctoGo is designed to be a zero-cost abstraction over the Propeller 2's unique 8-cog architecture.
-
Native Hardware Concurrency: The go keyword transpiles to a scoped block that claims a pooled slot holding the goroutine's stack and its arguments, then invokes _cogstart_C. The pool has one slot per available Cog, so the P2's 8-cog limit is enforced by construction; exceeding it is a runtime panic.
-
Hardware-Backed Channels: Channels (chan) are not software queues. Each is a rendezvous cell in Hub RAM guarded by one of the P2's native hardware locks (0-15), giving atomic, lock-step transfer. A channel is made as Go makes one,
make(chan T), its cell static at package level and the frame's in a function, with no allocator behind it, and a buffered one,make(chan T, n), has its n slots beside the cell, n a constant. Past the sixteenth channel the locks are shared rather than exhausted, which costs contention and nothing else — twenty-four channels each completing a rendezvous run correctly on a P2-EDGE. -
Zero-Allocation & No GC: OctoGo operates without a Garbage Collector. Memory scoping is strict (Hub RAM vs. Cog RAM), and slices are implemented as non-escaping views over fixed arrays.
-
Select Statements: The select statement is transpiled into an efficient polling loop, utilizing flexprop's _waitx yield instructions to prevent bus starvation during non-blocking hardware polling.
-
Implicit Namespaces: To keep the grammar clean and strictly LL(1), there is no package keyword. A package's namespace is implicitly inferred from its directory name, mapping cleanly to a single C translation unit.
OctoGo is a source-to-source compiler (transpiler) written in Go.
-
Frontend: Lexical analysis and parsing are generated via modernc.org/egg using a LL(1) grammar. The AST is represented as a zero-pointer, cache-local flat []int32 slice.
-
Semantic Pass: Uses Go 1.23+ iterators (
func(yield func(node) bool)) to traverse the AST, calculate memory offsets, and resolve scope. -
Transpilation: Emits standard C. The runtime it needs -- bounds and divide-by-zero traps, slice and channel helpers, the goroutine pool -- is emitted into the same file, per program, so only what is used is paid for.
-
Backend Generation: The ogo build command automatically feeds the emitted C into flexprop, delegating register allocation, instruction scheduling, and P2 binary generation to a proven, hardware-aware backend.
OctoGo compiles and runs real programs on real Propeller 2 hardware. Every change goes through a suite that builds each test program with the real backend, and before each release every one is also loaded onto a P2 and its serial output checked; there is no public CI, a board being part of the test, so each release's notes say what passed. It is also unfinished. This section is the honest inventory — please read it before deciding the compiler is broken.
Works (all of it exercised on hardware):
- Integers (sized and unsized, including 64-bit),
byte,rune,bool,string, structs, fixed arrays including multi-dimensional, slices, named types, channels. Structs may refer to themselves, to each other, and to a type declared later, so linked lists, trees and graphs build. A named type behaves as what it is defined over —type Name stringindexes, slices, ranges and compares as a string,type List []intas a slice,type Named Pointhas Point's fields and takes Point's literals — and carries its own methods. That holds for every kind a type can be defined over: a pointer dereferences, a function is called. A literal may name such a type,Row{1, 2, 3}fortype Row [3]int. - A slice literal stands wherever a value may —
sum([]int{1, 2, 3})— its backing array a local of the function that wrote it, with the lifetime that implies. - An array may be a slice's or a channel's element, of any rank:
[][2]int,chan [3]int,chan [2][3]int. C cannot spell one inline where a slice header's pointer or a rendezvous payload goes, so the element gets a generated typedef and everything that would copy it by value copies it through a pointer instead. Such a slice is made the same way as any other, by slicing a backing array —m[:]over a[4][2]intis a[][2]intover that storage, as isd[1][:]over one rank further in. - A pointer to an array,
*[3]int, which passes one by reference without a slice header. It abbreviates the dereference as Go does —p[i]is(*p)[i], and so arelen(p),range pandp[lo:hi]— while*pcopies the array and copying the pointer aliases it. No other pointer is indexable, which C would not say for itself. The dereference may also be written out and carry a suffix,(*p).xand(*p)[i]; for a pointer to a slice or a string that is the only spelling there is, an index on the pointer itself being no operation in either language. - Slicing, including the capacity bound:
pool[0:0:64]hands out a region of a package-level buffer that appending cannot grow past, which is how you sub-divide storage with no heap to allocate from. - Composite literals: structs positionally (
P{1, 2},P{}, nested) or by field name (P{y: 2}, any subset in any order, the rest zeroed), and arrays and slices positionally, by index or mixed ([4]int{10, 20},[]int{1, 2, 3},[5]int{0: 1, 4: 9}), including multi-dimensional ([2][3]int{{1, 2, 3}, {4, 5, 6}}) and at package scope, which is how you write a lookup table. One place a literal may not appear bare is the top level of anif,fororswitchheader, where{is the block — parenthesize there, as in Go:if p == (P{}) {. That is only for a literal whose type is a bare name;for _, v := range []int{1, 2, 3}needs nothing, a[not being able to begin a block. var(including several names and a value list),constwithiota,type, functions and methods with value or pointer receivers. A package may declare severalinitfunctions; they run in the order they are written, beforemain.- Named and unnamed parameters and results, multiple return values, naked returns.
Several values may be destructured out of a plain call, a method call or an
imported package's call alike,
b, ok := r.pop(). - A declared function used as a value: a variable, parameter, result, array
element or struct field of a function type holds one and a call through it is a
call,
chosen = add; chosen(1, 2), including one with several results.gostarts one too,go g(21). It becomes a C function pointer, so it costs nothing and allocates nothing, and one is always safe to send to another cog — it names code, not the frame it was made in. if/elseandswitchincluding an init statement (if v := f(); v > 0,switch v := f(); v), which may declare several names from one call (if v, ok := m.get(k); ok), allforforms includingrange,switchwith or without a guard,fallthrough,breakandcontinue(including labeled),defer(including in nested blocks, capturing its arguments).- The full operator set, compound assignment, multiple assignment, and equality on
strings, structs and arrays. A multiple assignment writes to anything a single one
can — an element, a field, a pointee — so
xs[i], xs[j] = xs[j], xs[i]swaps. - A call's result may be used directly: a field read off it (
mk().y), a method called on it, or an index into it (mk()[1],mk().d[1]) — an array result included, which may also be ranged, assigned and passed on whole. len,cap,append,copy,clear,min,max,makefor a fixed-capacity slice,panic,print/println.printf, the built-infmt.Printfwould be if there were afmtpackage:%d %x %X %o %b %q %U %s %t %f %e %E %g %G %c %v %T %%, over a format that must be a constant — which is what lets a wrong verb be refused where you wrote it instead of printing nonsense on the board.%Tcosts nothing for a static type and one pointer read for an interface, whose vtable carries its type's name.- A predeclared
Builderfor assembling a string at run time without a heap:NewBuilder(back[:])puts a write cursor over a byte array you own, andWriteString,WriteByte,WriteRune,Write,String,LenandResetbehave asstrings.Builder's do, results included, except that a write past the backing writes what fits and returns a non-nil error, "short write", rather than growing it — you chose the size — and thatStringis a view of the backing, overwritten by aResetand the writes after it, since there is no heap to copy it into. No interface holds one yet,anyamong them; a parameter of type*Buildertakes it. It is meant to move into thestringspackage asstrings.Builder. go,chanandselect, mapped to cogs and hardware locks. A method may be launched too,go w.run(ch), its receiver evaluated and copied where thegostands — including one reached through fields and indexes,go ws[i].run(ch), which is a worker per cog — as may an imported package's function,go driver.Poll(ch). Aselectmultiplexes receives and one send,case ch <- v:, and a receive clause may take the comma-ok form,case v, ok := <-ch:, whoseokis false once the channel is closed. A channel is made withmake(chan T), at package level or in a function, and a channel a function makes stays in it -- but formain's, which goroutines may share. The P2's locks are reachable directly through thep2package.- Runtime traps for out-of-range indexing and slicing, division and remainder by
zero, a shift by a negative count, appending past a slice's capacity, and cog
exhaustion, and a nil pointer dereference. Each prints
panic: <what>and stops every cog, as Go ends the whole program;--releasereboots the board instead, and--uncheckedomits the checks. - Small functions are inlined where they are called, a call being dear on this
target: an accessor of one line costs the check it makes and not the call as
well.
--no-inlineleaves it to the C backend. - A package is a directory:
ogo buildcompiles every.ogofile in it together, and a program may span several packages. A value of an imported package's struct type is written the way you would expect,geo.Point{1, 2}.
Does not work yet, in rough order of how likely you are to hit it:
- The standard library is seven packages.
stringsis the allocation-free part of Go's — the functions that answer a question about a string or return a substring of one, each meaning exactly what Go's of the same name means, checked by running the same program under Go — andbytesis the same part of Go's for a byte slice.mathis the whole of Go's on the same terms, its bodies Go's own pure Go source — the roots, powers, exponentials and logarithms, the trigonometric and hyperbolic functions and their inverses,Hypot,FMA,Erf,Gamma,Lgamma, the Bessel functions and the rest, Go's mathematical constants and the limits of the integer and the float types; a program carries only the functions it calls.errorsis Go's but forJoin(and there is nofmt.Errorf):Newmakes its error where the call stands, there being no heap, so a sentinel,var ErrTimeout = errors.New("timeout"), is declared at package level and a function returns one of those;errors.Newin a function makes an error that function may use and not return, and it is called where its error is wanted, never taken as a value.Is,AsandUnwrapwalk a tree of wrapped errors as Go's do.p2wraps thirty-three intrinsics (pin control, smart pins including the ADC, timing and the clock, the serial line in both directions, the hardware locks, the cog's LUT RAM),testingis Go's*testing.Tfor logging, failing and skipping, subtests ast.Runruns and go test -v prints them, and*testing.Bfor benchmarks asogo test -benchruns them,b.Loopandb.Runtoo (noCleanuporRunParallelyet; a subtest's name is a constant or lives as long, and a test runs 256 at most, eight deep),-runand-benchselecting subtests and sub-benchmarks as go test's do, andunsafehasunsafe.Pointer,Add,SliceandSizeof,AlignofandOffsetof, of the P2's layout. That is the whole of it. Your own packages do import and build; there is just little else to import yet. - One function's locals live in cog RAM, and there are 480 longs of it for all
of them together. A function big enough to exhaust that fails to build with the
backend's
fit 480 failed: pc is N— the P2's own limit rather than the compiler's — andogo buildnames the functions holding the most registers, the largest first. Small functions are inlined into their callers, so a call's arguments and locals count toward the frame of the function that makes it, and an--uncheckedbuild can need more registers than a checked one. Splitting the function into several is the fix, and is what the code wanted anyway. - A goroutine's stack is sized by the build, read off the backend's listing of
its frames and calls. Where every
gostatement of the program runs at most once — in no loop and no function literal, inmain, aninitor a function called once from such a place, as drivers are usually started — each has a slot of its own, sized for its goroutine; otherwise the pool is seven slots, each what the deepest goroutine needs. A program whose goroutine needs more than the largest slot, 8192 longs, is refused, andogo build --gostack Nsets every slot's size outright (64 to 8192 longs; the slots sit in hub RAM for the run). The listing cannot bound a goroutine RECURSING through its own calls, an interface's or a function value's, so a program starting one is refused unless--gostackgives the size. A call through a function value is taken to reach what the value's type may hold: the functions the program made values of that type -- of every type of the signature, where the program has a value of it written out,func(int) int, anywhere. Recursion works —mainruns on the cog's own stack, the rest of Hub RAM, and a goroutine on its pool slot's — and a recursion the build sees is checked as it runs: the build puts a check at the entry of each function on it, which panicsgoroutine stack overflowbefore the recursion writes past the end of its stack, keeping free what the panic needs, about 600 bytes. Before 2026-10-18 nothing did:mainrecursing too deep ran on past the top of Hub RAM in silence on a P2-EDGE, and a goroutine in a--gostackslot over the slot after it. A check costs three instructions onmain's cog and about ten on a goroutine's, and only a function on a recursion has one. Any other goroutine is fenced too, so one that overruns its slot and still returns ends with the same panic. - A method value in two shapes, neither an omission. One with a value
receiver: Go copies the receiver where the value is made and there is nowhere to
copy to, while binding the address instead would alias the variable, so the program
would answer differently the moment anything wrote to it. And one whose receiver
is not a package-level variable, whose address does not outlive the value. A
pointer-receiver method on a package variable is taken freely; the receiver is bound
at compile time, which is what keeps a function value one word
(
doc/funcval-cost.cprices the alternative). Every other function-valued form works,gothrough one included. - A local interface type, and a second local type of one name in one
function — every other
typedeclaration inside a function works, aliases and shadowing included. - A
rangeclause written with=accepts a variable or a struct field, not an element:for xs[0], a[0] = range xsis refused. - Every import path names a directory below the program's root, whoever writes
the import — and an
ogo.modfile is what says where that root is. It holds one line,module example.com/proj, and a build looks for the nearest one at or above the package it is given. Import paths are then written as Go writes them, module path included, socmd/firmwareandcmd/calibmay bothimport "example.com/proj/sensor"and share the one copy of it. Neither the importing package's own location nor the working directory takes any part in it. Without anogo.modthe directory you build is the root and paths are relative to it (import "sensor"), which reaches packages beside the main package and nothing outside it — nocmd/layout, and no package shared by two programs. There is no versioning and there are no external dependencies:ogo.modsays where the root is, and a line it does not implement, such asrequire, is refused rather than ignored. Nesting a package inside the one that imports it is not a layout either way —util/geo/is found as<module>/util/geo, not asgeo. A package directory must be lower case (letters, digits,_,-), though the module prefix may carry capitals, being a repository name:foo/andFoo/are one directory on macOS and Windows, so a program naming a capital is refused where it is written rather than where it is cloned. The same reasoning refuses the device names Windows reserves —con,prn,aux,nul,com0–com9,lpt0–lpt9— as a package directory and as a source file name (aux.ogoisaux): nothing on Windows may carry one, so a repository holdingcon/cannot even be checked out there.
Floating point (float32/float64) is supported, exponent literals included
(1e3, 1.5e-3), as Go defines it: float32 is IEEE single precision and
float64 IEEE double precision, 53 bits of significand, every operation rounded
to nearest even. The P2 has no floating-point hardware, so both are software: a
float32 the C toolchain's, a float64 routines the compiler writes into a
program that uses one -- about 6 KB with its printing, and an addition about 860
clocks, a multiplication 1,720 and a division 2,900 at 160 MHz, against a
float32's 660, 580 and 560. Printing is EXACT: the
compiler renders a float from its own exact decimal value rather than through the
target's printf, so %f, %e, %g and %v all produce the digits Go produces
-- 21.0 prints 21.000000, and %v gives the shortest form that round-trips to
the same float. (The compiler's integer-to-float conversion rounds an exact tie to even,
as IEEE 754 and Go do, so float32(16777217) is 16777216 here as it is in Go.) Constant expressions are evaluated exactly, as
Go evaluates them -- 0.1+0.2 == 0.3 is true, 7 / 2.0 is 3.5 -- and a constant
beside a float32 operand is a float32, so f == 0.3 compares two of them.
Not planned, because the target does not permit them: a garbage collector, a
heap, maps, closures that capture their environment (a function value is fine —
it is a pointer to code, not to a frame), and runtime string concatenation.
Constant string concatenation folds at compile time; to assemble one at run time,
write into a Builder over storage you own.
Having no heap has one consequence worth knowing before you meet it: a reference
must not outlive what it refers to. Where Go would move the referent to the heap and
say nothing, there is nowhere to move it to, so the program is refused instead —
returning a local's address or a slice backed by a local, storing either in a package
variable, or handing either to another cog as a go argument or through a channel. A
struct holding such a reference counts as one — and so does reading the reference
back out of it, b.d being the same slice header b carries, and an interface made
from one, any(&q) reaching q exactly as &q does — and the requirement
follows a parameter back to the call sites that chose the storage. main's
outermost block is the exception: it outlives every cog, since the end of main
stops them all, so a buffer declared there may go to a goroutine:
func main() {
var buf [64]byte
done := make(chan int)
go fill(buf[:], done) // main's outermost block outlives every cog
<-done
}
Anywhere else, declare the buffer at package scope and pass a slice of it, which is what the diagnostics ask for.
Everything else Go has is meant to be here eventually — anything missing above is work not yet done rather than a decision taken. Generics are the one open question: not supported, not planned, and not ruled out either.
Interfaces work: an interface value is a pointer to what it carries beside a
pointer to a statically emitted vtable, with type assertions and type switches on
top. A pointer is what goes into one -- var s Shape = &q, never = q -- because
there is no heap to copy into and a silently aliasing value form would mean
something Go does not; any expression already of pointer type goes in as it stands,
so a constructor's result does, var s Shape = New(). One may be written where a type is wanted rather than
declared with a name, func measure(s interface{ area() int }), and the empty one
is spelled any as well; identity is by method set, so any and interface{} are
one type, and a type switch's case may be one written out, case interface{ Unwrap() error }:. A conversion names the interface where the position does not, Shape(&q)
or any(&q), and builds the same two words the assignment does; a conversion to one
written out, interface{ M() }(x), is not supported yet. A method may
return several values, Next() (int32, bool). A struct may hold one in a field, or
embed one -- struct{ error } wraps a cause and passes as an error itself, the
promoted call dispatching through whatever the field holds -- and a call through a
nil interface panics, as Go's does. The
whole-program-optimization pass that would devirtualize the calls is still an open
question, and opinions are welcome.
