From 9da1baace91326547279583194c8052b02858d53 Mon Sep 17 00:00:00 2001 From: "Dominykas M." <26067386+Tasty-Kiwi@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:45:48 +0200 Subject: [PATCH 01/10] Implement wasm support for playground --- main.go => main_native.go | 2 + main_wasm.go | 12 ++++ wasm/wasm.go | 135 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) rename main.go => main_native.go (81%) create mode 100644 main_wasm.go create mode 100644 wasm/wasm.go diff --git a/main.go b/main_native.go similarity index 81% rename from main.go rename to main_native.go index be637a1..5c44d93 100644 --- a/main.go +++ b/main_native.go @@ -1,3 +1,5 @@ +//go:build !js + package main import ( diff --git a/main_wasm.go b/main_wasm.go new file mode 100644 index 0000000..9673d4f --- /dev/null +++ b/main_wasm.go @@ -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 {} +} diff --git a/wasm/wasm.go b/wasm/wasm.go new file mode 100644 index 0000000..ce11065 --- /dev/null +++ b/wasm/wasm.go @@ -0,0 +1,135 @@ +//go:build js + +package wasm + +import ( + "bufio" + "fmt" + "hybroid/alerts" + "hybroid/generator" + "hybroid/lexer" + "hybroid/parser" + "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 +} + +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() +} + +func compile(code string) (string, error) { + l := lexer.NewLexer(strings.NewReader(code)) + tokensList, err := l.Tokenize() + if err != nil { + return "", err + } + + if len(l.GetAlerts()) > 0 { + hasError := false + for _, a := range l.GetAlerts() { + if a.AlertType() == alerts.Error { + hasError = true + break + } + } + + msg := formatAlerts(l.GetAlerts(), code) + if hasError { + return "", fmt.Errorf("%s", msg) + } + fmt.Println(msg) // Log warnings + } + + p := parser.NewParser(tokensList) + program := p.Parse() + + if len(p.GetAlerts()) > 0 { + hasError := false + for _, a := range p.GetAlerts() { + if a.AlertType() == alerts.Error { + hasError = true + break + } + } + + msg := formatAlerts(p.GetAlerts(), code) + if hasError { + return "", fmt.Errorf("%s", msg) + } + fmt.Println(msg) + } + + gen := generator.NewGenerator() + generator.ResetGlobalGeneratorValues() + gen.Generate(program, nil) + + return gen.GetSrc(), 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" + } + code := args[0].String() + output, err := compile(code) + if err != nil { + 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") + js.Global().Set("hybroidCompile", compileWrapper()) +} From 05dd70ecaa08ac133361725a066d52f9c9184522 Mon Sep 17 00:00:00 2001 From: "Dominykas M." <26067386+Tasty-Kiwi@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:11:26 +0200 Subject: [PATCH 02/10] Refactor error handling in compileWrapper to return errors instead of printing --- wasm/wasm.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wasm/wasm.go b/wasm/wasm.go index ce11065..b0313b2 100644 --- a/wasm/wasm.go +++ b/wasm/wasm.go @@ -121,7 +121,8 @@ func compileWrapper() js.Func { code := args[0].String() output, err := compile(code) if err != nil { - fmt.Printf("unable to compile code: %s\n", err) + // Errors are returned instead of printed + // fmt.Printf("unable to compile code: %s\n", err) return err.Error() } return output From 801db80b206767bbd0f7ac83f384c2ccfab6e038 Mon Sep 17 00:00:00 2001 From: "Dominykas M." <26067386+Tasty-Kiwi@users.noreply.github.com> Date: Thu, 15 Jan 2026 18:08:03 +0200 Subject: [PATCH 03/10] Add project context and documentation for Hybroid Live --- GEMINI.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 GEMINI.md diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..49735b8 --- /dev/null +++ b/GEMINI.md @@ -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 [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`. From edba121bb69f08f96d648774328ce06e57b1bf81 Mon Sep 17 00:00:00 2001 From: "Dominykas M." <26067386+Tasty-Kiwi@users.noreply.github.com> Date: Thu, 15 Jan 2026 18:13:02 +0200 Subject: [PATCH 04/10] Update build_hybroid.py to accept platform argument and validate targets --- README.md | 2 +- utils/build_hybroid.py | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 726fd9b..bc008ce 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/utils/build_hybroid.py b/utils/build_hybroid.py index ce7044c..76f490c 100644 --- a/utils/build_hybroid.py +++ b/utils/build_hybroid.py @@ -1,5 +1,6 @@ import os import subprocess +import sys _CONFIGS = [ {"env": {"GOOS": "windows", "GOARCH": "arm64"}, "name": "windows-arm64"}, @@ -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) + + 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!") From ce3b9a4cbbf70e198ea916d851093294986e5fa8 Mon Sep 17 00:00:00 2001 From: "Dominykas M." <26067386+Tasty-Kiwi@users.noreply.github.com> Date: Thu, 15 Jan 2026 18:27:38 +0200 Subject: [PATCH 05/10] [wasm] import walker and add warnings to the output --- wasm/wasm.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/wasm/wasm.go b/wasm/wasm.go index b0313b2..3b06487 100644 --- a/wasm/wasm.go +++ b/wasm/wasm.go @@ -6,9 +6,11 @@ import ( "bufio" "fmt" "hybroid/alerts" + "hybroid/ast" "hybroid/generator" "hybroid/lexer" "hybroid/parser" + "hybroid/walker" "strings" "syscall/js" ) @@ -62,6 +64,8 @@ func formatAlerts(alertsList []alerts.Alert, source string) string { } func compile(code string) (string, error) { + var warnings strings.Builder + l := lexer.NewLexer(strings.NewReader(code)) tokensList, err := l.Tokenize() if err != nil { @@ -81,7 +85,7 @@ func compile(code string) (string, error) { if hasError { return "", fmt.Errorf("%s", msg) } - fmt.Println(msg) // Log warnings + warnings.WriteString(msg) } p := parser.NewParser(tokensList) @@ -100,14 +104,53 @@ func compile(code string) (string, error) { if hasError { return "", fmt.Errorf("%s", msg) } - fmt.Println(msg) + warnings.WriteString(msg) + } + + 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 len(w.GetAlerts()) > 0 { + hasError := false + for _, a := range w.GetAlerts() { + if a.AlertType() == alerts.Error { + hasError = true + break + } + } + + msg := formatAlerts(w.GetAlerts(), code) + if hasError { + return "", fmt.Errorf("%s", msg) + } + warnings.WriteString(msg) } gen := generator.NewGenerator() generator.ResetGlobalGeneratorValues() - gen.Generate(program, nil) - return gen.GetSrc(), nil + gen.SetEnv(w.Env().Name, w.Env().Type) + gen.GenerateUsedLibaries(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 { @@ -131,6 +174,6 @@ func compileWrapper() js.Func { } func init() { - fmt.Println("Hybroid Live for WebAssembly, v0.1.0") + fmt.Println("Hybroid Live for WebAssembly v0.1.0 has been initialized.") js.Global().Set("hybroidCompile", compileWrapper()) } From d85b7bf4b0f1e27f3b891ef5c309a3929c2439fa Mon Sep 17 00:00:00 2001 From: "Dominykas M." <26067386+Tasty-Kiwi@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:00:06 +0200 Subject: [PATCH 06/10] Update GEMINI.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- GEMINI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GEMINI.md b/GEMINI.md index 49735b8..72b1779 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -73,7 +73,7 @@ go test ./... * `generator/`: Lua code generation logic. * `lexer/`: Source code tokenization. * `lsp/`: Language Server Protocol implementation. -* `parser/`: recursive descent parser. +* `parser/`: Recursive descent parser. * `tokens/`: Token type definitions. * `utils/`: Python scripts for build, API generation, and maintenance. * `walker/`: Semantic analysis and type checking. From 71a433c96384c68c436caa477bacda96b51be46a Mon Sep 17 00:00:00 2001 From: "Dominykas M." <26067386+Tasty-Kiwi@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:00:25 +0200 Subject: [PATCH 07/10] Update wasm/wasm.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- wasm/wasm.go | 1 + 1 file changed, 1 insertion(+) diff --git a/wasm/wasm.go b/wasm/wasm.go index 3b06487..809ed53 100644 --- a/wasm/wasm.go +++ b/wasm/wasm.go @@ -29,6 +29,7 @@ func sourceToLines(source string) map[int][]byte { return lines } +// 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 From 28e808f6836cdf88b6bf4e43e2f769a27877bd8b Mon Sep 17 00:00:00 2001 From: "Dominykas M." <26067386+Tasty-Kiwi@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:01:41 +0200 Subject: [PATCH 08/10] Update wasm/wasm.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- wasm/wasm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wasm/wasm.go b/wasm/wasm.go index 809ed53..3cfca30 100644 --- a/wasm/wasm.go +++ b/wasm/wasm.go @@ -1,4 +1,4 @@ -//go:build js +//go:build js && wasm package wasm From 6d60c584550d6c5da8b807acd2909d8274c626a5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 18:18:53 +0200 Subject: [PATCH 09/10] =?UTF-8?q?Fix=20spelling:=20GenerateUsedLibaries=20?= =?UTF-8?q?=E2=86=92=20GenerateUsedLibraries=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Fix spelling: GenerateUsedLibaries -> GenerateUsedLibraries Co-authored-by: Tasty-Kiwi <26067386+Tasty-Kiwi@users.noreply.github.com> * Restore test directory files that were accidentally modified Co-authored-by: Tasty-Kiwi <26067386+Tasty-Kiwi@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Tasty-Kiwi <26067386+Tasty-Kiwi@users.noreply.github.com> --- evaluator/evaluator.go | 2 +- generator/generator.go | 2 +- wasm/wasm.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 4f3dfd3..5e7977c 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -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 { diff --git a/generator/generator.go b/generator/generator.go index 72d1632..3932d25 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -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") diff --git a/wasm/wasm.go b/wasm/wasm.go index 3cfca30..f75c9df 100644 --- a/wasm/wasm.go +++ b/wasm/wasm.go @@ -138,7 +138,7 @@ func compile(code string) (string, error) { generator.ResetGlobalGeneratorValues() gen.SetEnv(w.Env().Name, w.Env().Type) - gen.GenerateUsedLibaries(w.Env().UsedLibraries) + gen.GenerateUsedLibraries(w.Env().UsedLibraries) if w.Env().Type != ast.LevelEnv { gen.Generate(w.Program(), w.Env().UsedBuiltinVars) From 2f6d982b09ee7c95410b2671cd852aa109639ee0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 18:26:07 +0200 Subject: [PATCH 10/10] Refactor duplicate error-checking pattern in wasm compile function (#6) * Initial plan * Extract duplicate error-checking pattern into processAlerts helper Co-authored-by: Tasty-Kiwi <26067386+Tasty-Kiwi@users.noreply.github.com> * Remove unnecessary format string in processAlerts Co-authored-by: Tasty-Kiwi <26067386+Tasty-Kiwi@users.noreply.github.com> * Use errors.New instead of fmt.Errorf for pre-formatted strings Co-authored-by: Tasty-Kiwi <26067386+Tasty-Kiwi@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Tasty-Kiwi <26067386+Tasty-Kiwi@users.noreply.github.com> --- wasm/wasm.go | 72 ++++++++++++++++++++++------------------------------ 1 file changed, 30 insertions(+), 42 deletions(-) diff --git a/wasm/wasm.go b/wasm/wasm.go index f75c9df..4790067 100644 --- a/wasm/wasm.go +++ b/wasm/wasm.go @@ -4,6 +4,7 @@ package wasm import ( "bufio" + "errors" "fmt" "hybroid/alerts" "hybroid/ast" @@ -64,6 +65,29 @@ func formatAlerts(alertsList []alerts.Alert, source string) string { return sb.String() } +// 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 @@ -73,39 +97,15 @@ func compile(code string) (string, error) { return "", err } - if len(l.GetAlerts()) > 0 { - hasError := false - for _, a := range l.GetAlerts() { - if a.AlertType() == alerts.Error { - hasError = true - break - } - } - - msg := formatAlerts(l.GetAlerts(), code) - if hasError { - return "", fmt.Errorf("%s", msg) - } - warnings.WriteString(msg) + if err := processAlerts(l.GetAlerts(), code, &warnings); err != nil { + return "", err } p := parser.NewParser(tokensList) program := p.Parse() - if len(p.GetAlerts()) > 0 { - hasError := false - for _, a := range p.GetAlerts() { - if a.AlertType() == alerts.Error { - hasError = true - break - } - } - - msg := formatAlerts(p.GetAlerts(), code) - if hasError { - return "", fmt.Errorf("%s", msg) - } - warnings.WriteString(msg) + if err := processAlerts(p.GetAlerts(), code, &warnings); err != nil { + return "", err } walker.SetupLibraryEnvironments() @@ -118,20 +118,8 @@ func compile(code string) (string, error) { w.Walk() w.PostWalk() - if len(w.GetAlerts()) > 0 { - hasError := false - for _, a := range w.GetAlerts() { - if a.AlertType() == alerts.Error { - hasError = true - break - } - } - - msg := formatAlerts(w.GetAlerts(), code) - if hasError { - return "", fmt.Errorf("%s", msg) - } - warnings.WriteString(msg) + if err := processAlerts(w.GetAlerts(), code, &warnings); err != nil { + return "", err } gen := generator.NewGenerator()