Go whose errors the compiler checks, closed variants, and an exit to plain Go.
loGos provides a single toolchain for standard Go and the extended loGo/s language. Authoring in .lgo files adds compiler-enforced nil safety, compile-time field unrolling, read-only views, and structured concurrency without giving up Go idioms or runtime compatibility. The logos compiler checks these constructs, lowers them to standard Go code, and compiles them using a Go 1.27 backend, while plain go keeps upstream Go language semantics and command text (see README.go.md). When standard Go source is required, logos eject converts .lgo projects into standard Go modules with original comments preserved.
Why the extra syntax:
- Compile-time unrolling: inspect struct fields and branch on types with
comptimewithout run-time field lookups or heap allocations. - Read-only views: share slices and pointers without data copies while preventing writes through that name.
- Static nil safety: ensure locally declared pointers are non-nil and make missing values explicit with
T?. - Structured concurrency: bound goroutines in a
go groupthat coordinates cancellation, limits, and joins. - Type-directed dispatch: specialize generic functions with
const switch Twithout writing reflection by hand. - Closed unions: enforce exhaustive variant matching across sum types at compile time.
- Backend: Go 1.27 (
src/go.modspecifiesmodule stdwithgo 1.27). - Bootstrap toolchain: Go 1.24.6 or newer (
src/make.bashrequiresbootgo=1.24.6; source build verified with upstream Go 1.27.3 on Linux/amd64). - File extensions:
.lgofor loGo/s source packages;.gofor plain Go packages. - Module boundaries: One language per module; Go and loGo/s modules interoperate across imports.
- Ejection:
logos eject [-o DIR]emits an upstream-compatible Go 1.27 module tree. - Plain Go underneath: ejected output matches the compiler's lowering, proven by
TestEjectMatchesLoweringin src/cmd/compile/internal/logos/eject_test.go. - Go keeps its meaning: plain
goretains upstream language semantics and command text, proven by therun.bashregression gate (src/run.bash). - Imports cross kinds, one kind per module: Go and loGo/s packages import each other, and a module holds one language, proven by the
logos_interop.txtscript test (src/cmd/go/testdata/script/logos_interop.txt). - Diagnostics:
logos lintprovides in-compiler analysis for main-module packages without building binaries. - License: BSD-3-Clause (see LICENSE).
Contents: What is this · Install · Quick start · Features · Usage · Agent skills · Development · License
package main
import "fmt"
import "strconv"
func double() (int, error) {
n := strconv.Atoi("21")?
return 2 * n, nil
}
func main() { fmt.Println(double()) } // 42 <nil>In double(), the ? operator tests the error result of strconv.Atoi("21"). If Atoi returns a non-nil error, double() returns early with that error. When the call succeeds, n receives the integer value. Running logos run . prints 42 <nil>.
The full language definition is in logos-docs/SPEC.md.
Building loGos from source requires Git, Bash, and a working bootstrap Go installation of version 1.24.6 or newer (see src/make.bash).
-
Clone the repository:
git clone https://github.com/gosuda/loGos.git cd loGos -
Capture your clean host bootstrap Go root and build the toolchain from source:
unset GOROOT GOTOOLCHAIN upstream_go=$(command -v go) bootstrap_root=$("$upstream_go" env GOROOT) (cd src && GOROOT_BOOTSTRAP="$bootstrap_root" ./make.bash)
If your bootstrap Go 1.24.6+ binary is not on your
PATH, specifyGOROOT_BOOTSTRAP=/path/to/go1.24.6directly. -
Configure your environment from the repository root:
export GOROOT="$PWD" export PATH="$GOROOT/bin:$PATH" export GOTOOLCHAIN=local
-
Verify the installed binaries:
logos version go version
logos versionreports version details (for example,logos version go1.27.2 linux/amd64on Linux/amd64).go versionreportsgo version go1.27.2 linux/amd64.
Create a fresh module outside the toolchain tree:
mkdir ~/logos-hello && cd ~/logos-hello
logos mod init example.com/hello
cat > main.lgo <<'EOF'
package main
import (
"fmt"
"strconv"
)
func double() (int, error) {
n := strconv.Atoi("21")?
return 2 * n, nil
}
func main() {
fmt.Println(double())
}
EOF
logos run .Output:
42 <nil>
The logos eject command converts a loGo/s module into standard Go code that builds with any upstream Go 1.27 compiler:
logos eject -o ../logos-hello-go
cd ../logos-hello-goUse the same shell as the installation steps, which saved the upstream executable in upstream_go.
In a new shell, set upstream_go to the absolute path of your upstream Go executable.
If you used Go 1.24 through 1.26 for bootstrap, select an upstream Go 1.27 or newer installation instead.
Check its version before running the ejected module.
: "${upstream_go:?Set upstream_go to an upstream Go 1.27+ executable}"
env -u GOROOT -u GOTOOLCHAIN "$upstream_go" version
env -u GOROOT -u GOTOOLCHAIN "$upstream_go" run .Output:
42 <nil>
Both invocations print 42 <nil>. The ejected module is pure Go.
logos eject [-o DIR] requires that the target directory DIR either does not exist or is empty. Ejection translates .lgo sources into standard .go files, preserves source comments, places dependency modules under DIR/_logos_deps, and writes runtime support under DIR/_logos_deps/rt as module github.com/gosuda/loGos/rt. Upstream Go 1.27 or newer is required to compile the ejected output.
loGos enforces a single language per module:
- A module contains either
.lgopackages or.gopackages, never a mixture of both in the same module. - Packages interoperate across module boundaries through standard Go
importpaths:- A
.lgopackage can import standard library packages and external Go modules. Calls returning(T, error)bind directly to?. - A
.gopackage can import a.lgopackage. Exported functions and types are projected into standard Go signatures, with option types and closed unions represented as canonical Go types.
- A
- Property interfaces require an explicit assertion on the implementing struct declaration (
type S struct : I), not automatic structural subtyping.
const for unrolls loop bodies across struct fields during compilation, and val.[f] reads or writes each field directly without run-time field lookup. Field metadata such as f.Name and f.Tag.Get folds into string constants with no interface boxing. The compiler produces specialized code for each struct type.
main.lgo:
package main
import (
"comptime"
"fmt"
)
type Server struct {
Host string `json:"host"`
Port int `json:"port"`
}
func main() {
s := Server{Host: "db", Port: 5432}
var copy Server
const for _, f := range comptime.Fields[Server]() {
fmt.Println(f.Name, f.Tag.Get("json"))
copy.[f] = s.[f]
}
fmt.Println(copy)
}Output:
Host host
Port port
{db 5432}
Comptime inspection operates during compilation only; referencing comptime APIs at run time produces a compile error. Loop expansion accepts only comptime.Fields[T]() over a struct type, visits fields in declaration order, skips unexported fields from other packages, and defaults to a 4,096-node expansion limit.
A const view allows functions to read slices and pointers without defensive copies while the compiler prevents writes through that name. Converting string to const []byte performs no allocation; converting back is zero-copy when the bytes originated from a string or constant. The qualifier is checked during compilation and erased before code generation.
main.lgo:
package main
import (
"fmt"
"logos/rt"
)
type Config struct{ Port int }
func total(xs const []int) (t int) {
for _, x := range xs {
t += x
}
return
}
func main() {
xs := []int{1, 2, 3}
var view const []int = xs
fmt.Println(total(view))
xs[0] = 10
fmt.Println(total(view))
cfg := &Config{Port: 8080}
snap := rt.Freeze(cfg)
cfg.Port = 9090
fmt.Println(snap.Port, cfg.Port)
}Output:
6
15
8080 9090
A view is an alias to existing memory rather than an isolated snapshot, so modifications made through the original mutable holder remain visible through the view. When true isolation is required, logos/rt.Freeze deep-copies the value and returns a protected view at the cost of heap allocations.
Locally declared pointers written *T cannot be nil, eliminating defensive nil checks across local application code. Potential absence is typed explicitly as T?, requiring safe navigation (?.), coalescing (??), or nil guards before the compiler permits dereferencing. Field access on unnarrowed optional pointers is rejected at compile time.
main.lgo:
package main
import "fmt"
type User struct{ Name string }
func find(name string) *User? {
if name == "ada" {
return &User{Name: "Ada"}
}
return nil
}
func describe(u *User?) string {
if u == nil {
return "unknown"
}
return u.Name
}
func main() {
ada := find("ada")
bob := find("bob")
fmt.Println(ada?.Name ?? "anonymous")
fmt.Println(bob?.Name ?? "anonymous")
fmt.Println(describe(ada))
fmt.Println(describe(bob))
}Output:
Ada
anonymous
Ada
unknown
Pointers crossing the boundary from plain Go packages are platform pointers that may be nil. When this toolchain compiles the Go caller, a direct call that passes a nil pointer into a non-nil loGo/s parameter gets a run-time check via logos/rt.MustNonNil rather than failing the build. Calls through function values, method values, or interfaces are a documented exception.
A go group binds concurrent tasks to a lexical block where all child goroutines are joined before exit, cancellation propagates automatically on failure, and concurrency is capped by limit. Paired with logos/sync.Locked, data and mutex travel together so shared fields cannot be accessed unlocked. Unsupervised goroutines outside a group are rejected at compile time.
main.lgo:
package main
import (
"context"
"fmt"
"logos/sync"
)
type Counter struct{ Total int }
func run(ctx context.Context) error {
var c sync.Locked[Counter]
go group (ctx, limit = 2) {
for _, v := range []int{10, 20, 30} {
go {
c.WithLock(t => { t.Total += v })
}
}
}
var res int
c.WithLock(t => { res = t.Total })
fmt.Println("sum:", res)
return nil
}
func main() {
if err := run(context.Background()); err != nil {
panic(err)
}
}Output:
sum: 60
Structured concurrency does not provide universal data-race freedom; concurrent writes to shared storage still require locks, channels, or atomic values. Cancellation unblocks plain channel operations written directly in a child and wakes netpoll-backed network I/O, but blocking operations like time.Sleep, regular file I/O, and cgo calls cannot be interrupted and must run until completion before the group joins.
const switch T dispatches on the type argument of a generic function, so one function holds specialized paths without user-written reflection. Matching an exact type rebinds the value to that type, ~basic matches underlying types while preserving the named type, and interface cases expose the interface method set. Unmatched branches cost nothing when the type tag is concrete.
main.lgo:
package main
import "fmt"
type UserID int
type Word struct{ S string }
func (w Word) String() string { return w.S }
func Label[T any](v T) string {
const switch T {
case int:
return fmt.Sprintf("int:%d", v+1)
case ~int:
return fmt.Sprintf("userid:%v", v)
case fmt.Stringer:
return "stringer:" + v.String()
}
return "other"
}
func main() {
fmt.Println(Label(41))
fmt.Println(Label(UserID(7)))
fmt.Println(Label(Word{"hi"}))
fmt.Println(Label(1.5))
}Output:
int:42
userid:7
stringer:hi
other
const switch is a statement rather than an expression and does not permit a default arm or multiple types per case; unhandled types fall through to statements following the block. Compile-time branch folding occurs only for concrete type tags and comptime generics, while standard generic functions evaluate the switch at run time.
Closed unions define a fixed set of variants as an interface value type, allowing exhaustive type switch expressions without silent fallthrough. Property interfaces provide field-style contracts where structs implement accessors only through an explicit declaration assertion (: I). The compiler validates that all cases and interface properties match at compile time.
main.lgo:
package main
import "fmt"
type Value interface{ int | string }
func tag(v Value) string {
return switch v.(type) {
case int => "int"
case string => "str"
case nil => "nil"
}
}
type Creature interface { const Name string; HP int }
type Monster struct : Creature {
Name string
HP int
}
func main() {
m := Monster{Name: "gorg", HP: 100}
var c Creature = &m
c.HP -= 10
var v Value = 7
fmt.Println(tag(v), tag(nil), c.Name, m.HP)
}Output:
int nil gorg 90
Closed unions cannot be extended outside their declaring package and use Go's standard interface representation at run time rather than a tagged-union layout. Property interfaces require pointers when writing to properties, and type switch expressions over unions must explicitly handle every member plus nil unless a default case is provided.
Multi-value function results can be packed into a single first-class tuple value without declaring a standalone struct type. Tuple values can be passed as arguments, stored in channels and slices, accessed by field names (t.host), or unpacked via destructuring assignments. This removes the boilerplate of temporary struct declarations for internal multi-value records.
main.lgo:
package main
import "fmt"
func resolve(service string) (host string, port int) {
if service == "api" {
return "api.example.com", 443
}
return "localhost", 8080
}
func url(e (host string, port int)) string {
return fmt.Sprintf("http://%s:%d", e.host, e.port)
}
func main() {
api := resolve("api")
fmt.Println(api.host, api.port)
fmt.Println(url(api))
host, port := api
fmt.Println(host, port)
queue := make(chan (host string, port int), 1)
queue <- api
next := <-queue
fmt.Println(next.host, next.port)
}Output:
api.example.com 443
http://api.example.com:443
api.example.com 443
api.example.com 443
Field names are defined by the tuple type or function signature rather than in literal expressions, and positional indexing like t.0 is rejected by the compiler. Under the hood, tuples lower to generated structs, and printing a tuple value directly displays the underlying struct representation.
Small fixed-size numeric arrays support arithmetic operators (+, -, *, /, &, |, ^) and reductions (.sum(), .min(), .max(), .dot()) directly without manual loops; the operators allocate nothing. Reductions use a language-specified pairwise evaluation order so floating-point results remain deterministic across supported vector lowerings and scalar fallback. The compiler eliminates manual vectorization loops while guaranteeing identical numeric results across amd64, arm64, and scalar lowerings.
main.lgo:
package main
import "fmt"
func main() {
a := [4]float64{1, 2, 3, 4}
b := [4]float64{5, 6, 7, 8}
add := a + b
mul := a * b
fmt.Println("add", add)
fmt.Println("mul", mul)
fmt.Println("sum", a.sum(), "min", a.min(), "max", a.max(), "dot", a.dot(b))
p := [4]int32{17, -19, 100, 7}
q := [4]int32{5, 4, -3, 2}
fmt.Println("div", p/q)
fmt.Println("intsum", p.sum(), "intdot", p.dot(q))
}Output:
add [6 8 10 12]
mul [5 12 21 32]
sum 10 min 1 max 4 dot 70
div [3 -4 -33 3]
intsum 105 intdot -277
Element-wise math is supported on arrays only, not slices, and requires a power-of-two element count whose total size does not exceed 64 bytes. Vectorized execution requires GOEXPERIMENT=simd on supported architectures and instruction sets; otherwise the compiler falls back to unrolled scalar instructions with identical numeric results and no throughput guarantee.
The language also provides these concise extensions defined in logos-docs/SPEC.md:
- Error propagation (
n := f()?orf()?): unwraps values or returns early on non-nil error from functions returning(T, error)(logos-docs/SPEC.md Sec 7.1). - Error cleanup (
errdefer f()): registers deferred cleanup executed only when the enclosing function exits with an error, a panic, or Goexit (logos-docs/SPEC.md Sec 7.1). - String interpolation (
$"user {name}: {count}"): embeds formatted expressions directly within string literals (logos-docs/SPEC.md Sec 2). - Iterator chains (
xs.filter(...).map(...).take(n)): runs functional slice transformations with predicate and mapping closures (logos-docs/SPEC.md Sec 2). - Literal addressing (
&100,&f()): takes the address of scalar literals and function call returns without temporary variables (logos-docs/SPEC.md Sec 2). - Struct field defaults (
type Config struct { Port int = 8080 }): evaluates default field values when struct literals omit fields (logos-docs/SPEC.md Sec 2).
Day to day, logos mirrors the go command: logos run, logos build, and logos test work on loGo/s modules the way their go counterparts work on Go modules. Two commands are specific to loGo/s. logos eject [-o DIR] writes the module as standard Go (see Eject to standard Go). logos lint runs the in-compiler checks below.
The logos lint command runs in-compiler checks through escape analysis and stops before code generation. It emits diagnostics without writing executable binaries:
logos lint [build flags] [--pedantic] [--json-diagnostics] [packages]- Target scope:
logos lintinspects packages within the main module only. The standard library and external module dependencies are never linted. - Language checks:
- In
.lgopackages, the compiler runs all registered compiler rules. - In standard
.gopackages, the compiler runs language-neutral rules only.
- In
- Directives:
//logos:allow(<rule>): Placed directly above a declaration or statement to suppress adenyorwarndiagnostic. In Go packages,forbidrules run atdenyseverity and can be silenced by this directive; in.lgopackages,forbidrules cannot be silenced.//logos:zeroalloc: Placed above a function declaration as a static escape-analysis site contract requiring the compiler to reject local heap allocations at that site (see logos-docs/LINT-RULES.md).
- Flags:
--pedantic: Enables pedantic diagnostics overriding module configuration.--json-diagnostics: Emits one JSON object per diagnostic line on standard error.
For the normative rule set design and individual rule definitions, see:
- logos-docs/LINT-RULESET.md: Proposed normative lint design for Go and loGo/s.
- logos-docs/LINT-RULES.md: Detailed reference catalog of in-compiler rules.
One row per shipped form, with milestone IDs from logos-docs/IMPLEMENTATION.md. The full language definition is in logos-docs/SPEC.md.
| Milestone | Form | Summary |
|---|---|---|
| M1 | ?, errdefer, eject |
Checked error propagation with early return, failure-only cleanup, and error traces; logos eject writes standard Go. |
| M2 | Closed unions | Fixed variant sets with exhaustive type switches. |
| M7 | const if, const for, const switch T, comptime.ViewAs |
Compile-time branching, field unrolling, type dispatch, and read-only views. |
| M8 | go group (ctx, limit = N) |
Structured concurrency with automatic joins, cancellation, and a concurrency cap. |
| M9 | logos lint, --json-diagnostics |
In-compiler checks that stop before code generation, with one JSON object per diagnostic. |
| M10 | logos next, logos context |
Rewrites a Go module toward loGo/s; prints the declaration and signatures for one symbol. |
| M11 | Generated MarshalJSON / UnmarshalJSON |
Static JSON methods on tagged structs with encoding/json-matching output. |
| M13 | -O |
Optimization tiers from -O=0 to -O=3 plus Os and Oz, synthetic PGO, clone specialization, and identical code folding. Every backend, runtime, and tooling improvement that ships applies to Go packages as well as loGo/s packages under logos and plain go. |
| M14 | Fixed-array SIMD | Element-wise operators and reductions on small fixed-size arrays behind GOEXPERIMENT=simd. |
| M16 | WASM profile | Flag or directive only: --wasm-features=stack-switching, --wasm-loader, and the //logos:wasmimport / //logos:wasmexport directives. |
| M17 | Smart stack | Flag only: GOEXPERIMENT=logosstack starts goroutines at per-call-site sizes. |
| M18 | Arena and bump allocation | Flag only: GOEXPERIMENT=logosarena, or [build] arena = true in logos.toml. |
| M19 | GC modes | Flag only: -gc= with greentea, immix, zgc, wasm, batch, or epsilon. |
| M20 | Fast-CGo | Directive and flag only: //logos:cgo_fastcall (or #cgo noescape with #cgo nocallback) behind GOEXPERIMENT=logosfastcgo. |
| M21 | logos lsp |
Editor setup: gopls proxy mapping .lgo positions, with inlay hints, hover, and code actions. |
| M22 | Verification suites | Directive only: //logos:zeroalloc rejects any heap allocation in the marked function. |
The plugins/logos/ directory packages skills for coding assistants:
logos: Author, build, test, and eject loGo/s modules.logos-review: Review.lgocode against compiler rules, diagnostics, and language invariants.logos-from-go: Guide migration of existing plain Go packages to loGo/s syntax.logos-performance: Assist with zero-copy views and cgo entry points.
To provide coding agents with project conventions and compiler constraints, copy plugins/logos/templates/AGENTS.md into the target repository root. See plugins/logos/ for plugin structure and usage documentation.
Follow the Install instructions to compile the toolchain from source. Run the commands below from the toolchain repository root, not the example module.
Each command enters src before it runs the tests:
(cd src && go test -count=1 cmd/internal/testdir -run 'Test/logos/')
(cd src && go test -count=1 -short cmd/compile/internal/types2 -run TestLogos)
(cd src && go test -count=1 cmd/go -run 'Script/^logos_')- Fork the repository on GitHub.
- Create a feature branch (
git checkout -b feature/my-feature). - Add test fixtures under
test/logos/run/orsrc/internal/types/testdata/logos/covering new syntax or compiler behavior. - Verify that targeted tests pass before submitting changes.
- Commit your changes with concise, descriptive commit messages adhering to standard Go conventions.
- Open a Pull Request on GitHub.
See CONTRIBUTING.md and README.go.md for contribution guidelines.
Standard Go source code is licensed under the BSD-style license in LICENSE. loGo/s additions and toolchain modifications are distributed under the same license terms.