Skip to content
94 changes: 94 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Hybroid Live - Project Context

## Project Overview

**Hybroid Live** is a statically-typed programming language designed specifically for creating content for the game **PewPew Live**. It transpiles to Lua, which is the scripting language used by the game.

**Key Goals:**
* Provide a better developer experience than raw Lua.
* Add features missing in Lua (classes, enums, strict typing).
* Optimize for PewPew Live specifics (tick loops, fixed-point math).
* Provide robust error messages.

**Status:** Alpha (expect breaking changes).

## Architecture

The project is written in **Go** and follows a standard compiler architecture:
1. **Lexer (`lexer/`)**: Tokenizes source code.
2. **Parser (`parser/`)**: Constructs the AST (`ast/`).
3. **Walker (`walker/`)**: Performs semantic analysis, type checking, and scope resolution.
4. **Generator (`generator/`)**: Transpiles the AST into Lua code.
5. **LSP (`lsp/`)**: Provides Language Server Protocol support for editors (VS Code).

## Development Environments

Hybroid supports distinct "environments" that dictate available standard libraries and compilation behavior:
* `Level`: For game levels (access to `fmath`, restricted Lua stdlib).
* `Mesh`: For generating meshes (full math support).
* `Sound`: For generating sounds.

## Build & Usage

### Prerequisites
* Go 1.23+
* Python 3 (for build scripts)

### Building the CLI
To build the native CLI executable:
```bash
go build -o hybroid main_native.go
```

### Running the CLI
```bash
./hybroid <command> [arguments]
```
Common commands:
* `init`: Initialize a new Hybroid project.
* `build`: Compile Hybroid code to Lua.
* `watch`: Watch for changes and recompile.

### Building for Release
Use the Python helper script:
```bash
python utils/build_hybroid.py
```

### Testing
Run standard Go tests:
```bash
go test ./...
```

## Directory Structure

* `alerts/`: Error reporting system (diagnostics, pretty printing).
* `ast/`: Abstract Syntax Tree node definitions.
* `cli/`: Implementation of CLI commands.
* `core/`: Core data structures (Queue, Stack, Span).
* `docs/`: Documentation website (Astro).
* `evaluator/`: Constant evaluation and testing logic.
* `examples/`: Sample Hybroid projects and code snippets.
* `generator/`: Lua code generation logic.
* `lexer/`: Source code tokenization.
* `lsp/`: Language Server Protocol implementation.
* `parser/`: Recursive descent parser.
* `tokens/`: Token type definitions.
* `utils/`: Python scripts for build, API generation, and maintenance.
* `walker/`: Semantic analysis and type checking.
* `wasm/`: WASM bindings for the web playground.

## Key Files

* `main_native.go`: Entry point for the CLI.
* `spec.md`: The Hybroid language specification.
* `hybconfig.toml`: Project configuration file (seen in examples).
* `go.mod`: Go dependencies.

## Conventions

* **Language:** Go for the toolchain, Python for build scripts.
* **Style:** Follows standard Go formatting (`gofmt`).
* **Testing:** Uses Go's built-in testing framework.
* **Documentation:** Maintained in `docs/` and `spec.md`.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Check out the "Getting Started" section of the [Hybroid documentation site](http

## Building for release

Run `utils/build_hybroid.py`.
Run `utils/build_hybroid.py [platform]`.

## License

Expand Down
2 changes: 1 addition & 1 deletion evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func (e *Evaluator) Action(cwd, outputDir string) error {
//fmt.Println("Generating the lua code...")

gen.SetEnv(walker.Env().Name, walker.Env().Type)
gen.GenerateUsedLibaries(walker.Env().UsedLibraries)
gen.GenerateUsedLibraries(walker.Env().UsedLibraries)
if e.files[i].FileName == "level" {
gen.GenerateWithBuiltins(walker.Program())
} else if e.walkerList[i].Env().Type != ast.LevelEnv {
Expand Down
2 changes: 1 addition & 1 deletion generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func (gen *Generator) GetSrc() string {
return gen.src.String()
}

func (gen *Generator) GenerateUsedLibaries(usedLibraries []ast.Library) {
func (gen *Generator) GenerateUsedLibraries(usedLibraries []ast.Library) {
for _, v := range usedLibraries {
str := v.String()
gen.src.Write("local ", str, " = ", str, "\n")
Expand Down
2 changes: 2 additions & 0 deletions main.go → main_native.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !js

package main

import (
Expand Down
12 changes: 12 additions & 0 deletions main_wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//go:build js && wasm

package main

import (
_ "hybroid/wasm"
)

func main() {
// Prevent the program from exiting so the JS functions remain registered
select {}
}
23 changes: 18 additions & 5 deletions utils/build_hybroid.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import subprocess
import sys

_CONFIGS = [
{"env": {"GOOS": "windows", "GOARCH": "arm64"}, "name": "windows-arm64"},
Expand Down Expand Up @@ -36,15 +37,27 @@ def _build_platform(platform, env, cwd):
if __name__ == "__main__":
# Go to the hybroid root directory to build
os.chdir(os.path.dirname(__file__) + "/..")

# Clean previous builds
os.makedirs("build", exist_ok=True)
for file in os.listdir("build"):
os.remove("build/" + file)

print(f"[+] Starting sequential build of Hybroid")
target = None
if len(sys.argv) > 1:
target = sys.argv[1]
valid_targets = [c["name"] for c in _CONFIGS]
if target not in valid_targets:
print(f"Error: Target '{target}' not found.")
print(f"Available targets: {', '.join(valid_targets)}")
sys.exit(1)

# Clean previous builds only if building everything
if not target:
for file in os.listdir("build"):
os.remove("build/" + file)

Copilot AI Jan 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file removal logic will fail if the build directory contains subdirectories, as os.remove only works with files. Consider using os.path.isfile() to filter only files, or handle the exception when attempting to remove a directory.

Suggested change
os.remove("build/" + file)
path = os.path.join("build", file)
if os.path.isfile(path):
os.remove(path)

Copilot uses AI. Check for mistakes.

print(f"[+] Starting build of Hybroid")

for config in _CONFIGS:
if target and config["name"] != target:
continue
_build_platform(config, os.environ, os.getcwd())

print("[+] Build job completed!")
168 changes: 168 additions & 0 deletions wasm/wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//go:build js && wasm

package wasm

import (
"bufio"
"errors"
"fmt"
"hybroid/alerts"
"hybroid/ast"
"hybroid/generator"
"hybroid/lexer"
"hybroid/parser"
"hybroid/walker"
"strings"
"syscall/js"
)

// Helper to reconstruct lines from source code
func sourceToLines(source string) map[int][]byte {
lines := make(map[int][]byte)
scanner := bufio.NewScanner(strings.NewReader(source))
lineNum := 1
for scanner.Scan() {
// Copy the bytes because scanner reuses the buffer
txt := scanner.Text()
lines[lineNum] = []byte(txt)
lineNum++
}
return lines
}

Comment thread
Tasty-Kiwi marked this conversation as resolved.
// formatAlerts converts a list of alerts into a formatted string with ANSI color codes, including error locations and code snippets.
func formatAlerts(alertsList []alerts.Alert, source string) string {
lines := sourceToLines(source)
var sb strings.Builder

for _, alert := range alertsList {
msg := ""
switch alert.AlertType() {
case alerts.Error:
msg = fmt.Sprintf("[light_red][bold]error[%s]: [reset]", alert.ID())
case alerts.Warning:
msg = fmt.Sprintf("[light_yellow][bold]warning[%s]: [default]", alert.ID())
}
sb.WriteString(msg)
sb.WriteString(fmt.Sprintf("[bold]%s[reset]\n", alert.Message()))

// Location
tokensList := alert.SnippetSpecifier().GetTokens()
if len(tokensList) > 0 {
sb.WriteString(fmt.Sprintf(" at line %d:%d\n", tokensList[0].Line, tokensList[0].Column.Start))
}

// Snippet
snippet := alert.SnippetSpecifier().GetSnippet(lines, alert)
sb.WriteString(snippet)

// Note
if alert.Note() != "" {
sb.WriteString(fmt.Sprintf("note: %s\n", alert.Note()))
}
sb.WriteString("\n")
}
return sb.String()
}

Copilot AI Jan 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compile function lacks a documentation comment. As the core functionality of this package, it should have a comment explaining its purpose, parameters, and return values. Consider adding: '// compile transpiles Hybroid source code to Lua, collecting and formatting any warnings or errors encountered during lexing, parsing, and semantic analysis.'

Suggested change
// compile transpiles Hybroid source code to Lua, collecting and formatting any
// warnings or errors encountered during lexing, parsing, and semantic analysis.
// The input parameter code is the Hybroid source as a string. It returns the
// generated Lua source code, or a non-nil error if a fatal issue is detected.

Copilot uses AI. Check for mistakes.
// processAlerts processes a list of alerts, returning an error if any are errors,
// or accumulating warnings in the provided strings.Builder.
func processAlerts(alertsList []alerts.Alert, source string, warnings *strings.Builder) error {
if len(alertsList) == 0 {
return nil
}

hasError := false
for _, a := range alertsList {
if a.AlertType() == alerts.Error {
hasError = true
break
}
}

msg := formatAlerts(alertsList, source)
if hasError {
return errors.New(msg)
}
warnings.WriteString(msg)
return nil
}

func compile(code string) (string, error) {
var warnings strings.Builder

l := lexer.NewLexer(strings.NewReader(code))
tokensList, err := l.Tokenize()
if err != nil {
return "", err
}

if err := processAlerts(l.GetAlerts(), code, &warnings); err != nil {
return "", err
}

p := parser.NewParser(tokensList)
program := p.Parse()

if err := processAlerts(p.GetAlerts(), code, &warnings); err != nil {
return "", err
}

walker.SetupLibraryEnvironments()
w := walker.NewWalker("main.hyb", "main.lua")
w.SetProgram(program)

// Single file compilation, so no other walkers to share context with
walkers := make(map[string]*walker.Walker)
w.PreWalk(walkers)
w.Walk()
w.PostWalk()

if err := processAlerts(w.GetAlerts(), code, &warnings); err != nil {
return "", err
}

gen := generator.NewGenerator()
generator.ResetGlobalGeneratorValues()

gen.SetEnv(w.Env().Name, w.Env().Type)
gen.GenerateUsedLibraries(w.Env().UsedLibraries)

if w.Env().Type != ast.LevelEnv {
gen.Generate(w.Program(), w.Env().UsedBuiltinVars)
} else {
gen.Generate(w.Program(), []string{})
}

res := gen.GetSrc()
if warnings.Len() > 0 {
res = warnings.String() + "[default]============\n\n" + res
}

return res, nil
}

func compileWrapper() js.Func {
compileFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
return "expected 1 argument"
}
if args[0].Type() != js.TypeString {
return "expected string"
Comment on lines +148 to +151

Copilot AI Jan 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error messages returned to JavaScript callers are very generic ('expected 1 argument', 'expected string'). Consider making these more descriptive, such as 'hybroidCompile expects exactly 1 argument (code as string)' to provide better developer experience for JavaScript users of the API.

Suggested change
return "expected 1 argument"
}
if args[0].Type() != js.TypeString {
return "expected string"
return "hybroidCompile expects exactly 1 argument (code as string)"
}
if args[0].Type() != js.TypeString {
return "hybroidCompile expects its first argument to be a string containing the code to compile"

Copilot uses AI. Check for mistakes.
}
code := args[0].String()
output, err := compile(code)
if err != nil {
// Errors are returned instead of printed
// fmt.Printf("unable to compile code: %s\n", err)
return err.Error()
}
return output
})
return compileFunc
}

func init() {
fmt.Println("Hybroid Live for WebAssembly v0.1.0 has been initialized.")
js.Global().Set("hybroidCompile", compileWrapper())
}
Loading