diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..72b1779 --- /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`. 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/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/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/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!") diff --git a/wasm/wasm.go b/wasm/wasm.go new file mode 100644 index 0000000..4790067 --- /dev/null +++ b/wasm/wasm.go @@ -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 +} + +// 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() +} + +// 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" + } + 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()) +}