Elvelt

Frontends

Go Frontend

Go Frontend

The Go frontend is a BraceFrontendBase subclass with dedicated Lexer and Parser helpers for Go-specific syntax: short variable declarations :=, channels <-, select, go and defer statements, composite literals, and for-range. The MVP lowers channels, goroutines, and select to synchronous semantics: every channel op completes immediately, every go runs inline, every defer runs at the call site.

File Lines Purpose
frontends/go/GoFrontend.h 1,409 Frontend class with all method bodies inline
frontends/go/Lexer.h / .cpp ~12 / 280 Go tokenizer
frontends/go/Parser.h / .cpp 84 / 690 Short-var decl, type declarations, channel send, for-range, statement kinds
frontends/go/GoStdLib.h 83 findStdLibMapping for fmt.*, math.*, os.*, strings.*

Language registration

// GoFrontend.h:12-13
std::string languageName() const override { return "go"; }
std::vector<std::string> fileExtensions() const override { return {".go"}; }

Only .go is recognised.


Preprocessing

GoFrontend::parse (line 709) runs joinGoStatements(source) (line 740) before BraceFrontendBase::parse. The joiner keeps go f(), defer f(), and select { … } on a single logical line so that the base class's automatic semicolon insertion heuristic doesn't break them up.

package main is preserved as mod->metadata["go_package"] = "main" (line 56, isIgnoredLine skips the package line so it isn't parsed as code).


// GoFrontend.h:35-43
return {
    {"fmt.Println", true, true},
    {"fmt.Print", true, false},
    {"fmt.Printf", true, false},
    {"print", true, false},
    {"println", true, true}
};

Features (file:line)

Short variable declarations :=

tryParseVarDecl (line 60) first tries go::Parser::parseShortVarDecl, which recognises:

x := 42
x, y := 1, 2
x int := f()        // with explicit type
ch := make(chan int)

The result is a VariableDeclNode with metadata["decl_origin"]=":=", plus metadata["go_type"]="<typeExpr>" when an explicit type is given.

var / const declarations

Recognised by go::Parser::parseVariableDeclaration (line 104). const sets isConst=true. Type metadata is preserved.

Channels: make, send, receive

make(chan T) / make(chan T, n)

tryBuildChannelInitializer (line 795) detects channel constructors in short-var and var initializers. The lowered IR is an ObjectLiteralNode with a __chan_value__ member holding a queue:

ch := make(chan int)        // ch is an mpl_object with __chan_value__: []
ch <- 42                    // AssignmentNode: ch.__chan_value__ = 42
<-ch                        // FunctionCallNode("__go_chan_recv__", [ch])

Channel send ch <- value

tryParseLanguageSpecificStatement (line 287-302) detects ch <- value before parseStatement so it isn't routed through parseExpr. Lowered to:

AssignmentNode(target=MemberAccessNode(ch, "__chan_value__"), value=parseExpr(value), op="=")

Channel receive x, ok := <-ch; <-ch; x := <-ch

Recognised by the expression parser; emits FunctionCallNode("__go_chan_recv__", [ch]) or with optional ok flag.

select { case … default: … }

parseSelectBlock (line 1054) handles the MVP synchronous semantics: each case's body is emitted in declaration order, unconditionally.

select {
case v := <-ch:
    fmt.Println(v)
case ch <- 42:
    fmt.Println("sent")
default:
    fmt.Println("nothing")
}

Lowers to a BlockNode with metadata["go_select"]="true" containing the case bodies in order, with a __go_select_choose__ runtime hook that the backend can expand into a real select on platforms that support it.

go f() and defer f()

Statement Where Lowered to
go f(args) line 361 FunctionCallNode("f", args) (inline; MVP is synchronous)
defer f(args) line 347 FunctionCallNode("f", args) (inline; MVP runs at defer site, not at function return)

The MVP semantics mean there is no actual concurrency and no LIFO ordering of deferred calls. Real semantics would require async lowering to a coroutine or thread-pool backend extension (see limitations.md).

Composite literals

tryBuildCompositeLiteralInitializer (line 819) detects:

[]int{1, 2, 3}
[3]string{"a", "b", "c"}
map[string]int{"a": 1, "b": 2}
Point{X: 1, Y: 2}

Lowers to ObjectLiteralNode (for maps and structs) or ArrayLiteralNode (for slices/arrays).

for / for-range

Three forms supported by tryParseGoForRange (line 1208):

Form Behaviour
for i := 0; i < n; i++ { … } ForNode(init, condition, increment, body) — standard C-style
for cond { … } WhileNode(cond, body)
for [k [, v]] := range X { … } BlockNode with metadata["go_for_range"]="true", key+value bindings declared inside, body wrapped in a 1-iteration ForNode with BreakNode (MVP semantics — body executes once)
for range X { … } Same as above without bindings

The for-range MVP wraps the body in a single iteration. The C++ backend on platforms that support iteration semantics can lower this further.

Type declarations

Form Where IR
type Name struct { … } line 224 ClassDeclNode(name=Name, isStruct=true, members=…)
type Name interface { … } line 271 InterfaceDeclNode(name=Name)
type Name = Underlying line 277 TypeAliasDeclNode(name=Name, typeExpr=Underlying)
type Name Underlying line 277 TypeAliasDeclNode(name=Name, typeExpr=Underlying, isExported=…)

go::Parser::parseTypeDeclaration is the helper.

Function declarations

tryParseFunctionDeclHeader (line 481) recognises:

func Name(params) returnType { … }
func (r *Type) Name(params) returnType { … }   // method
func Name(params) (resultA, resultB) { … }     // multi-return

Multi-return functions set metadata["multi_return"]="true". Receiver methods set metadata["go_receiver"]="<receiver>".

The main() function gets metadata["cpp.inline_entrypoint"]="true" so the C++ backend inlines its body at the program entry point (line 507).

Struct fields

tryParseClassFieldDecl (line 513) parses Go struct fields:

type Point struct {
    X, Y int          // multiple names sharing a type
    Name string
    Inner              // embedded (anonymous)
    *Other             // pointer to embedded
}

Embedded fields get metadata["go_embed"]="true" and metadata["go_type"] set. Multi-name fields return only the first name (the C++ backend synthesises the rest from the struct tag).

Imports

import "fmt"
import alias "fmt"
import (
    "fmt"
    "os"
)

Lowered to a VariableDeclNode (alias = variable name) with metadata["decl_origin"]="import" and a FunctionCallNode("__go_import__", [module, alias]) (line 207-219).

panic / recover

Built-in Where IR
panic(expr) line 422 ThrowNode(value=parseExpr(expr))
recover() line 436 FunctionCallNode("__go_recover__")

Other Go built-ins

tryParseLanguageSpecificStatement (line 444-476) maps:

Built-in IR
make(...) FunctionCallNode("__go_make__", args)
new(T) FunctionCallNode("__go_new__", args)
len(x) FunctionCallNode("__go_len__", [x])
cap(x) FunctionCallNode("__go_cap__", [x])
append(s, x) FunctionCallNode("__go_append__", [s, x])
copy(dst, src) FunctionCallNode("__go_copy__", [dst, src])
delete(m, k) FunctionCallNode("__go_delete__", [m, k])
close(ch) FunctionCallNode("__go_close__", [ch])
complex(r, i) FunctionCallNode("__go_complex__", [r, i])
imag(z) FunctionCallNode("__go_imag__", [z])
real(z) FunctionCallNode("__go_real__", [z])

fallthrough, goto, break, continue, return

Statement Where IR
fallthrough line 376 FunctionCallNode("__go_fallthrough__")
goto label line 381 FunctionCallNode("__go_goto__", [label])
break line 390 BreakNode
break label line 390 BreakNode(metadata["go_label"]=label)
continue line 398 ContinueNode
continue label line 398 ContinueNode(metadata["go_label"]=label)
return expr line 406 ReturnNode(value=parseExpr(expr))

Goroutine lambdas: func() { … }()

tryParseGoLambdaCall (line 982) detects inline goroutine-style lambdas:

go func() {
    fmt.Println("async")
}()

Lowers to FunctionCallNode of a synthesized lambda.


Standard library mapping (GoStdLib.h)

The complete table at frontends/go/GoStdLib.h:40-72:

Go MPL module.function
fmt.Println(...) Console.WriteLine(...)
fmt.Printf(...) Console.Write(...)
fmt.Print(...) Console.Write(...)
fmt.Sprintf(...) String.Format(...)
math.Abs(x) Math.Abs(x)
math.Floor(x) Math.Floor(x)
math.Ceil(x) Math.Ceil(x)
math.Round(x) Math.Round(x)
math.Sqrt(x) Math.Sqrt(x)
math.Pow(x, y) Math.Pow(x, y)
math.Max(a, b) Math.Max(a, b)
math.Min(a, b) Math.Min(a, b)
math.Log(x) Math.Log(x)
math.Log2(x) Math.Log2(x)
math.Log10(x) Math.Log10(x)
math.Sin(x) Math.Sin(x)
math.Cos(x) Math.Cos(x)
math.Tan(x) Math.Tan(x)
os.Exit(code) Process.Exit(code)
os.Getwd() Environment.GetCurrentDirectory()
os.Getenv(name) Environment.GetVariable(name)
os.Setenv(name, val) Environment.SetVariable(name, val)
strings.ToLower(s) String.ToLower(s)
strings.ToUpper(s) String.ToUpper(s)
strings.TrimSpace(s) String.Trim(s)
strings.HasPrefix(s, p) String.StartsWith(s, p)
strings.HasSuffix(s, p) String.EndsWith(s, p)
strings.Split(s, sep) String.Split(s, sep)

IR examples

Example 1 — channel and goroutine

ch := make(chan int)
go func() {
    ch <- 42
}()
v := <-ch

Lowers to (MVP synchronous semantics):

VariableDeclNode(name="ch",
  initializer=ObjectLiteralNode(metadata=["go_chan_type"="int"]))
FunctionCallNode(name="<lambda1>",
  arguments=[],
  metadata=["go_goroutine"="true"])
  body:
    AssignmentNode(target=MemberAccessNode(ch, "__chan_value__"),
                   value=LiteralNode(42))
VariableDeclNode(name="v",
  initializer=FunctionCallNode("__go_chan_recv__", [IdentifierNode("ch")]))

Example 2 — type declaration with methods

type Counter struct {
    n int
}

func (c *Counter) Inc() { c.n++ }
func (c *Counter) Value() int { return c.n }

Lowers to:

ClassDeclNode(name="Counter", isStruct=true)
  members:
    VariableDeclNode(name="n", class_field="true", metadata=[go_type="int"])
  methods:
    FunctionDeclNode(name="Inc", metadata={ go_receiver="c *Counter" })
      body: AssignmentNode(target=MemberAccessNode(c, "n"),
                           value=BinaryOpNode("+", MemberAccessNode(c, "n"), 1), op="=")
    FunctionDeclNode(name="Value", metadata={ go_receiver="c *Counter" }, returnType="int")
      body: ReturnNode(value=MemberAccessNode(c, "n"))

Example 3 — select statement

select {
case v := <-ch:
    fmt.Println("got", v)
case ch <- 1:
    fmt.Println("sent")
default:
    fmt.Println("default")
}

Lowers to a BlockNode(metadata["go_select"]="true") containing the three case bodies in order with __go_select_choose__ markers.


What is not supported (Go-specific gaps)

  • Real concurrency: go is synchronous, no thread pool.
  • LIFO defer: deferred calls run at the defer site.
  • Real select: cases execute in order, no readiness check.
  • Generic type parameters (func F[T any](x T)) — passed through as raw identifier.
  • Embedding with promoted methods: only the embedded field is emitted; method promotion is a backend concern.
  • Channel direction (chan<- T / <-chan T) — direction is dropped.
  • iota in const blocks.
  • type switch — emitted as a regular switch.
  • defer with closure capture — runs synchronously, may not capture the right value if used inside a loop.