Skip to main content

Command Palette

Search for a command to run...

The Compiler Stopped Talking About Go: GALA in August and September (0.72 → 0.79)

Twelve releases moved GALA's errors out of generated Go and into your .gala source, then into JSON, the IDE, and a Claude Code plugin.

Updated
13 min readView as Markdown
M
Staff Software Engineer at Snowflake. Expert in scalable architecture, cloud infra, and security. Passionate problem-solver and mentor.

Until 0.75, this GALA program compiled, linked, and printed v=1:

val x = 1
Println(s"v=${x +}")

The trailing + just vanished. Invalid source produced a working binary.

The cause was plumbing, not anything deep. The string-interpolation re-parser removed the default error listeners and never installed its own. On top of that, the expression rule wasn't anchored to end-of-input, so x + parsed as x and stopped there. Nobody noticed because nobody writes ${x +} on purpose. It still counts as the worst kind of compiler bug: the program was wrong and nothing said so.

It stood out, but it wasn't unusual. Any language that compiles to another language picks up a whole class of these bugs, where the error is real but shows up in the wrong place, in the wrong vocabulary, or not at all. Go is a very good backend and a mediocre narrator. If the transpiler lets a mistake through, go build catches it and reports it against a generated file you never wrote, with type names you never used.

GALA has shipped twelve releases since the last of these posts, 0.73.0 through 0.79.0. Most of the work had one goal: report the problem in your file, in GALA's terms, to whatever is reading it. These days that might be you, your editor, or an AI agent editing the code for you.


Errors in your file, not in gen/

Here's the before picture. A GALA package under internal/ was always private to its parent tree, because Go enforces that rule. But Go enforced it on the generated code, so the error looked like this:

gen\main.gen.go:6:8: use of internal package gala-build-workspace/gen/sub/internal/deep not allowed

That names a file you didn't write and a path that only exists inside the build workspace. In 0.73 the check moved into GALA and became GALA-E0041, reported at your import line:

error[GALA-E0041]: package "example.com/myapp/sub/internal/deep" is internal to "example.com/myapp/sub" and cannot be imported from "example.com/myapp"
  --> main.gala:3:8
  |
3 | import "example.com/myapp/sub/internal/deep"
  |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ internal packages are private to their parent tree

The rule is Go's, exactly as cmd/go applies it. Only whole path elements count (internalize stays public), the last internal element wins, and the standard library gets no exemption.

0.75 applied the same idea to three mistakes that people coming from other languages make all the time. All three used to fail badly.

Unparenthesized lambda parameters. x => x * 2 used to produce up to four cascading parser errors, and none of them mentioned parentheses. Now you get one:

error[GALA-E0042]: lambda parameters must be parenthesized
  --> main.gala:5:20
  |
5 |     Println(xs.Map(x => x * 2))
  |                    ^ use `(x) => ...`
  |
  = hint: use `(x) => ...`; GALA always parenthesizes a lambda's parameter list, including a single parameter

Why not just accept the bare form? Because case x => body would become ambiguous, and the parser would silently pick the wrong reading. I'd rather have a clear error than a grammar that sometimes guesses wrong.

A type name called like a constructor. Array(1, 2, 3) used to build a corrupt struct literal from three of Array's four private fields, and the failure came back in Go's words. Now:

error[GALA-E0043]: Array is a type, not a constructor
  --> main.gala:6:14
  |
6 |     val xs = Array(1, 2, 3)
  |              ^^^^^ use `ArrayOf(...)`
  |
  = hint: use `ArrayOf(...)`; GALA constructs values through named functions, so a type name is never callable

A method the type doesn't have. This one used to come from go build, reported against the generated expression, including auto-unwrap .Get() calls the user never wrote. Now GALA reports it and suggests the closest real method:

error[GALA-E0044]: Array has no method Sise
  --> main.gala:7:16
  |
7 |     Println(xs.Sise())
  |                ^^^^ did you mean `Size`?
  |
  = hint: did you mean `Size`?

When nothing is close, it lists what the type does declare.

Two fixes in the same release go with these. First, a diagnostic raised inside an interpolation now points at the right line. A bare len inside s"..." on line 6 used to put its caret on package main, because the embedded expression was re-parsed in a fresh stream where everything sat on line 1. Second, gala build and gala run now print a file, line, and source frame for syntax errors. Before, only gala transpile did.


The same diagnostics, for a machine

A framed error with a caret is great for a person reading a terminal. For a program it's text that has to be scraped. More and more of the programs reading compiler output are coding agents, and scraping is where they make mistakes.

So 0.75 added a structured form of each diagnostic. With --json on build, run and transpile, the Sise error above becomes:

{
  "diagnostics": [
    {
      "severity": "error",
      "code": "GALA-E0044",
      "message": "Array has no method Sise",
      "hint": "did you mean `Size`?",
      "file": "main.gala",
      "line": 7,
      "column": 16,
      "endColumn": 20,
      "docsUrl": "https://gala.fyi/docs/errors/gala-e0044/"
    }
  ]
}

Two lookup commands came with it:

  • gala explain <code> prints the full reference page for any diagnostic, offline. It takes GALA-E0044, E0044 or just 44, and --list enumerates every code.
  • gala doc <package>[.<Type>] prints what a package exports. That turns "what can I call on this?" into a lookup instead of a guess.

The website also serves an llms.txt now. It covers the places where GALA looks like Go or Scala but behaves differently, which is where generated code most often goes wrong.


Documentation reaches the editor

The standard library has over 1,500 doc comments. Until 0.76, the lexer threw every one of them away, so no tool could show them.

Now they flow through. GALA uses Go's convention exactly: a // run directly above a declaration documents it. So the comments you already write show up in hover, completion and signature help:

func (Option) GetOrElse(defaultValue T) T

GetOrElse returns the option's value if the option is Some, otherwise returns
the result of evaluating defaultValue.
defaultValue: the default value to return if the option is empty.

*Package: std*

That last name: description line is a parameter doc. Signature help shows it next to that parameter. It's only treated as a parameter doc when name matches a real parameter, so Note: and Example: stay part of the prose.

Adding documentation exposed how much hover was missing. Before 0.76 it returned nothing for methods, struct fields, local bindings, package aliases and import paths. In practice, every method call in every GALA file hovered to nothing. Hover now uses the same type resolution as completion, so the two can't disagree about what an expression is.

The point releases after that were mostly about builder chains, the style every GALA server, client and config API is written in:

gsrv.NewServer().
    WithName("gala-kv").
    WithShutdownTimeout(timeout).
    ServeTCPOn(addr, (c) => Serve(c, st)) match { ... }

In 0.76.0, hovering over any link in that chain showed nothing. The chain starts with a package qualifier rather than a value, and the resolver looked that qualifier up as if it were a receiver type. It found no methods and gave up on every link after it. Fixing that turned up more problems:

  • Go to definition looked for the dot on the cursor's own line. In a chain, that line has no dot; it's at the end of the line above. It only seemed to work because it fell back to a global name search, which returned whichever type happened to declare a method with the same name.
  • A comment above a statement was being read as part of it. Ordinary sentences end in a period, and the line joiner treats a trailing . as a member access.
  • val shutdownTimeout = 15 * time.Second hovered as int. Arithmetic took its type from the left operand. Go's rule is that an untyped constant takes the type of the typed operand. The generated Go was always correct, because Go re-typed it. GALA's own inference was wrong, and hover and inlay hints are built on it.

0.77 made completion write your code, not just list names. Completing a call inserts its required parameters as named arguments, with a tab stop for each:

srv.NewServer().
    WithName(name = ▮)
    ServeTCPOn(addr = ▮, handler = ▮)

Parameters with defaults are left out, because the call can omit them. Inside an argument list, completion offers the parameters you haven't passed yet first.

This work also turned up a transpiler bug. Methods with their own type parameters ignored named arguments and bound everything by position:

arr.FoldLeft(f = (acc, x) => acc + x, initial = 0)
// was: Array_FoldLeft(arr, func(acc any, x any) any {...}, 0) — swapped, untyped

That output swapped the arguments, and it used any, which GALA's codegen is never allowed to emit unless the source asks for it. Generic methods now bind by name, fill in defaults, and report unknown, duplicate and missing arguments like every other call. The line above prints 10.


A plugin for Claude Code

The last two releases, 0.78 and 0.79, both came out today, and they connect all of the above to an agent.

GALA now ships a Claude Code plugin that runs gala lsp. While Claude edits .gala files, it gets two things:

  • Diagnostics after every edit. Parse errors, non-exhaustive matches, GALA-E* checks like the data-race check from the last post: all of them reach Claude on its next tool call, so it fixes them without waiting for a build.
  • Lookups instead of guesses. Claude's LSP tool can ask the server for hover (inferred types and docs), go to definition, find references, and document or workspace symbols.
/plugin marketplace add martianoff/gala
/plugin install gala@gala

gala new projects are already set up for it. They include a .claude/settings.json that declares the marketplace, so Claude Code offers the plugin once you trust the folder.

Building it surfaced some LSP server gaps that no editor had exposed. Programs split across files had no cross-file navigation. workspace/symbol wasn't implemented. The outline found names by substring, which put main on the package main line. And the shutdown response had neither result nor error, which JSON-RPC 2.0 forbids. All four are fixed in 0.78.

0.79 fixed a less obvious problem: having tools doesn't mean Claude will use them. With only the plugin, Claude reliably received diagnostics but rarely asked the server anything. On a real task it guessed at APIs and grepped the standard library cache. The plugin now includes a short skill (about 74 tokens per session) that tells Claude to use its Edit tool rather than cat >> (only editor edits trigger diagnostics), to look up types through the server, and to finish with gala build.

I ran the same task in fresh sessions with and without the skill. Without it, Claude made zero LSP calls. With it, Claude went from a type's definition to its method list to a hover, edited through Edit, built, and did it for about half the cost. That's one task, so treat it as an anecdote, not a benchmark. Still, the difference was large enough to ship.

The limit is the same as in the terminal. Diagnostics only cover what GALA's transpiler checks. An error that only the Go compiler finds still needs gala build, and the skill says so.


The rest

Conventional Go project layouts build from the root. For a project with packages under internal/ and the program under cmd/<name>/, gala build at the root used to fail with no Go files in <workspace-hash>/gen. Now it finds the main package itself. If there are several, it lists them and asks you to pick one. gala test handles the same layout too.

Standing guards against type problems. Since 0.74, several guards run continuously instead of relying on individual fixes. One checks that no generated variable, field, map or slice element widens to any unless the source asked for it. Another checks that every error code has a docs page, and every docs page has a code. There's also a guard that transformer state gets restored on error paths. And GALA_WARN_TYPES=1 lists every expression the transpiler couldn't type. That list works as a regression detector rather than a backlog. Every entry today is in a program that compiles and runs, because Go's type checker finishes the job, so an increase is the thing to act on.

An inference bug that depended on a different file. GALA's strings package shares a name with Go's. Once any file in a package imported GALA's, a sibling file's Go strings.SplitN was typed as returning Array[string], while the code generator still emitted Go's []string. Adding a file could break a different file you hadn't touched. Fixed in 0.73.1: a qualified call now resolves through that file's own imports.

The IntelliJ plugin works on 2025.3+ again. On newer platforms, every PSI-based feature crashed as soon as you opened a .gala file. The cause was a final class in the platform that the ANTLR adaptor subclasses. That adaptor has exactly one release on Maven Central, so upgrading it wasn't an option, and the plugin no longer uses it. The plugin's unit tests also run in CI again.

SECURITY.md. Security reports now go through private GitHub advisories. The scope includes soundness holes in the guarantees GALA advertises (the race checker, immutability, exhaustiveness). Running untrusted GALA is explicitly out of scope, because GALA is not a sandbox.


Where this leaves things

With a transpiled language, it's easy to treat the target language's compiler as the real checker, and the language's own checks as nice extras. That works right up until a user has to debug your generated code to understand their own mistake.

These twelve releases pushed the other way. Mistakes are now reported by GALA, against your source, with a code you can look up and a fix you can apply. That same diagnostic now reaches the terminal, the JSON output, the IDE, and an agent halfway through an edit, without any of them scraping text.

Some rough edges remain. Anything Go catches that GALA doesn't still shows up as a Go error after a full build. The type-resolution inventory isn't empty. And the agent integration is new enough that its best evidence so far is one task. Still, the direction is clear: by the time Go sees the code, it should have nothing left to report.

The error reference is at gala.fyi/docs/errors, the Claude Code plugin setup is in ide/claude-code, and the playground runs GALA without installing anything.

GALA From the Ground Up

Part 13 of 13

A ten-part technical blog series exploring GALA — a modern programming language that brings sealed types, pattern matching, monadic error handling, and immutable-by-default semantics to the Go ecosystem. Each post takes a single concept, shows the problem it solves with real code, compares it to idiomatic Go, and is honest about trade-offs. Written for Go developers who want more expressiveness and functional programmers who want Go's runtime.

Start from the beginning

Pattern Matching in Go: How GALA Brings Sealed Types and Exhaustive Matching to the Go Ecosystem

Go is a language built on simplicity. But simplicity has costs, and one cost Go developers feel acutely is the lack of pattern matching and algebraic data types. If you have ever modeled a closed set