diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..515859a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +tools/ast_explorer/static/main.wasm +tools/ast_explorer/static/wasm_exec.js + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2ed2167 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +punch: + go build ./cmd/punch/ + +run-ast-explorer: + bash ./scripts/build_wasm.sh + go run ./tools/ast_explorer/ + +clean: + rm punch diff --git a/README.md b/README.md index a1681ea..735615d 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,43 @@ # PUNCH πŸ₯Š -`punch` is a hobby programming language. At the moment, `punch` targets wasm. +`punch` is a hobby programming language. > I'm mainly working on this as a learning experience. +[demo playground](https://dfirebaugh.github.io/punch/) + ### Build -Compile a punch program to wasm: +To build you will need [golang installed](https://go.dev/doc/install). + +To run code locally, you will need `node` or `bun` installed in your PATH. + ```bash -# build the wasm file -punch -o ./examples/adder/adder ./examples/adder/adder.p -# execute the wasm file -cd ./examples/adder -node adder.js -``` +go build ./cmd/punch/ -> a `.wat` file and `.ast` file will also be output for debug purposes +./punch ./examples/simple.pun # output: Hello, World! +``` #### Functions ```rust // function declaration -bool is_best(i8 a, i8 b) +bool is_best(i32 a, i32 b) // simple function -i8 add(i8 a, i8 b) { +i8 add(i32 a, i32 b) { return a + b } // exported function -pub i8 add_two(i8 a, i8 b) { +pub i32 add_two(i32 a, i32 b) { return a + b } // multiple return types -(i8, bool) add_eq(i8 a, i8 b) { +(i32, bool) add_eq(i32 a, i32 b) { return a + b, a == b } // no return -main() { +fn main() { println("hello world") } ``` @@ -52,12 +53,8 @@ if a && b { #### Assignment ```rust -i8 a = 42 -i16 b = 42 i32 c = 42 i64 d = 42 -u8 e = 42 -u16 f = 42 u32 g = 42 u64 h = 42 f32 k = 42.0 @@ -111,7 +108,7 @@ import ( "fmt" ) -main() { +fn main() { fmt.Println("hello, world!") } ``` @@ -119,25 +116,25 @@ main() { #### Status > work in progress -| Feature | ast | wasm | -| - | - | - | -| function declaration | βœ… | βœ… | -| function calls | βœ… | βœ… | -| function multiple returns | βœ… | ❌ | -| if/else | βœ… | βœ… | -| strings | βœ… | βœ… | -| integers | βœ… | βœ… | -| floats | βœ… | βœ… | -| structs | βœ… | βœ… | -| struct access | ❌ | ❌ | -| loops | ❌ | ❌ | -| lists | ❌ | ❌ | -| maps | ❌ | ❌ | -| pointers | ❌ | ❌ | -| enums | ❌ | ❌ | -| modules | ❌ | ❌ | -| type inference | βœ… | βœ… | -| interfaces | ❌ | ❌ | +| Feature | ast | wasm | js | +| - | - | - | - | +| function declaration | βœ… | βœ… | βœ… | +| function calls | βœ… | βœ… | βœ… | +| function multiple returns | ❌ | ❌ | ❌ | +| if/else | βœ… | βœ… | βœ… | +| strings | βœ… | βœ… | βœ… | +| integers | βœ… | βœ… | βœ… | +| floats | βœ… | βœ… | ❌ | +| structs | βœ… | βœ… | βœ… | +| struct access | βœ… | ❌ | βœ… | +| loops | βœ… | ❌ | βœ… | +| lists | ❌ | ❌ | ❌ | +| maps | ❌ | ❌ | ❌ | +| pointers | ❌ | ❌ | ❌ | +| enums | ❌ | ❌ | ❌ | +| modules | ❌ | ❌ | ❌ | +| type inference | ❌ | ❌ | ❌ | +| interfaces | ❌ | ❌ | ❌ | ## Reference - [WebAssembly Text Format (WAT)](https://webassembly.github.io/spec/core/text/index.html) diff --git a/cmd/punch/main.go b/cmd/punch/main.go index 92c9bf4..6a12897 100644 --- a/cmd/punch/main.go +++ b/cmd/punch/main.go @@ -3,20 +3,26 @@ package main import ( "flag" "fmt" + "log" "os" + "os/exec" - "github.com/dfirebaugh/punch/internal/compiler" + "github.com/dfirebaugh/punch/internal/emitters/js" + "github.com/dfirebaugh/punch/internal/lexer" + "github.com/dfirebaugh/punch/internal/parser" ) func main() { var outputFile string - var outputWat bool + var outputTokens bool + var outputJS bool var outputAst bool var showHelp bool flag.StringVar(&outputFile, "o", "", "output file (default: .wasm)") - flag.BoolVar(&outputWat, "wat", false, "output WebAssembly Text Format (WAT) file") + flag.BoolVar(&outputTokens, "tokens", false, "output tokens") flag.BoolVar(&outputAst, "ast", false, "output Abstract Syntax Tree (AST) file") + flag.BoolVar(&outputJS, "js", false, "outputs js to stdout") flag.BoolVar(&showHelp, "help", false, "show help message") flag.Parse() @@ -36,41 +42,91 @@ func main() { panic(err) } - wat, wasm, ast := compiler.Compile(filename, string(fileContents)) + l := lexer.New(filename, string(fileContents)) + + if outputTokens { + tokens := l.Run() + for _, token := range tokens { + fmt.Println(token) + } + return + } + + p := parser.New(l) + program := p.ParseProgram(filename) + ast, err := program.JSONPretty() + if err != nil { + panic(err) + } if outputFile == "" { outputFile = filename } - if outputWat { - err = os.WriteFile(outputFile+".wat", []byte(wat), 0o644) + if outputAst { + fmt.Printf("%s\n", ast) + } + + t := js.NewTranspiler() + jsCode, err := t.Transpile(program) + if err != nil { + log.Fatalf("error transpiling to js: %v", err) + } + + if outputJS { + fmt.Printf("%s\n", jsCode) + return + } + + bunPath, err := exec.LookPath("bun") + if err != nil { + log.Printf("bun is not available on the system, trying node: %v", err) + nodePath, err := exec.LookPath("node") if err != nil { - panic(err) + log.Fatalf("neither bun nor node is available on the system. Please install one of them.") } - } + cmd := exec.Command(nodePath, "--input-type=module") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr - if outputAst { - err = os.WriteFile(outputFile+".ast", []byte(ast), 0o644) + nodeStdin, err := cmd.StdinPipe() if err != nil { - panic(err) + log.Fatalf("failed to open pipe to node: %v", err) } + + go func() { + defer nodeStdin.Close() + nodeStdin.Write([]byte(jsCode)) + }() + + err = cmd.Run() + if err != nil { + log.Fatalf("failed to run node: %v", err) + } + return } - err = os.WriteFile(outputFile+".wasm", wasm, 0o644) + cmd := exec.Command(bunPath, "-e", jsCode) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err = cmd.Run() if err != nil { - panic(err) + log.Fatalf("failed to run bun: %v", err) } } func printUsage() { - fmt.Println("Usage:", os.Args[0], "[-o output_file] [--wat] [--ast] ") + fmt.Println("Usage:", os.Args[0], "[-o output_file] [--tokens] [--wat] [--ast] [--js] ") fmt.Println("Options:") fmt.Println(" -o string") fmt.Println(" output file (default: .wasm)") - fmt.Println(" --wat") - fmt.Println(" output WebAssembly Text Format (WAT) file") + fmt.Println(" --tokens") + fmt.Println(" output tokens") fmt.Println(" --ast") fmt.Println(" output Abstract Syntax Tree (AST) file") + fmt.Println(" --js") + fmt.Println(" output Javascript to stdout") fmt.Println(" --help") fmt.Println(" show help message") } diff --git a/examples/adder/adder.js b/examples/adder/adder.js index fa744df..c92c026 100644 --- a/examples/adder/adder.js +++ b/examples/adder/adder.js @@ -1,6 +1,6 @@ const fs = require('node:fs'); -const wasmBuffer = fs.readFileSync('./adder.wasm'); +const wasmBuffer = fs.readFileSync('./adder.punch.wasm'); const encode = function stringToIntegerArray(string, array) { const alphabet = "abcdefghijklmnopqrstuvwxyz"; diff --git a/examples/example.pun b/examples/example.pun new file mode 100644 index 0000000..9f4136a --- /dev/null +++ b/examples/example.pun @@ -0,0 +1,30 @@ +pkg main + +bool is_eq(i32 a, i32 b) { + return a == b +} + +i32 add_two(i32 x, i32 y, i32 z) { + println("x =", x, "y =", y, "z =", z) + println("Hello, World!") + println("some other string") + return x + y +} + +i32 add_four(i32 a, i32 b, i32 c, i32 d) { + return a + b + c + d +} + +pub fn hello(bool is_hello) { + if is_hello { + println("hello, again") + } + if is_eq(2, 2) { + println("2 is equal") + } + + println("adding some nums:", add_two(4,5,2)) +} + + +hello(true) diff --git a/examples/factorial.pun b/examples/factorial.pun new file mode 100644 index 0000000..e48087f --- /dev/null +++ b/examples/factorial.pun @@ -0,0 +1,11 @@ +pkg main + +i32 factorial(i32 n) { + i32 result = 1 + for i32 i = 1; i <= n; i = i + 1 { + result = result * i + } + return result +} + +println("Factorial of 5 is {}", factorial(5)); diff --git a/examples/fib.pun b/examples/fib.pun new file mode 100644 index 0000000..478ab98 --- /dev/null +++ b/examples/fib.pun @@ -0,0 +1,13 @@ +pkg main + +i32 fibonacci(i32 n) { + if n == 0 { + return 0 + } + if n == 1 { + return 1 + } + return fibonacci(n - 1) + fibonacci(n - 2) +} + +println("Fibonacci(10) is {}", fibonacci(10)) diff --git a/examples/greet.pun b/examples/greet.pun new file mode 100644 index 0000000..974d22d --- /dev/null +++ b/examples/greet.pun @@ -0,0 +1,7 @@ +pkg main + +pub fn greet(str name) { + println("Hello,", name) +} + +greet("World!") diff --git a/examples/if.pun b/examples/if.pun new file mode 100644 index 0000000..600d2e6 --- /dev/null +++ b/examples/if.pun @@ -0,0 +1,11 @@ +pkg main + +pub fn check_number(i32 x) { + if x > 0 { + println(x, "is positive") + } +} + +check_number(5) + +check_number(-3) diff --git a/examples/loop.pun b/examples/loop.pun new file mode 100644 index 0000000..9b8b72f --- /dev/null +++ b/examples/loop.pun @@ -0,0 +1,9 @@ +pkg main + +pub fn count_to(i32 n) { + for i32 i = 1; i <= n; i = i + 1 { + println(i) + } +} + +count_to(5) diff --git a/examples/math.pun b/examples/math.pun new file mode 100644 index 0000000..888de9f --- /dev/null +++ b/examples/math.pun @@ -0,0 +1,13 @@ +pkg main + +pub fn math_operations() { + i32 a = 10 + i32 b = 20 + println("Addition: ", a + b) + println("Subtraction: ", a - b) + println("Multiplication: ", a * b) + println("Division: ", b / a) + println("Modulus: ", b % a) +} + +math_operations() diff --git a/examples/recursion.pun b/examples/recursion.pun new file mode 100644 index 0000000..478ab98 --- /dev/null +++ b/examples/recursion.pun @@ -0,0 +1,13 @@ +pkg main + +i32 fibonacci(i32 n) { + if n == 0 { + return 0 + } + if n == 1 { + return 1 + } + return fibonacci(n - 1) + fibonacci(n - 2) +} + +println("Fibonacci(10) is {}", fibonacci(10)) diff --git a/examples/return.pun b/examples/return.pun new file mode 100644 index 0000000..6e07290 --- /dev/null +++ b/examples/return.pun @@ -0,0 +1,7 @@ +pkg main + +i32 square(i32 n) { + return n * n +} + +println("Square of 4 is", square(4)) diff --git a/examples/simple.pun b/examples/simple.pun new file mode 100644 index 0000000..cc46db6 --- /dev/null +++ b/examples/simple.pun @@ -0,0 +1,7 @@ +pkg main + +fn greet(str name) { + println("Hello,", name) +} + +greet("World!") diff --git a/examples/struct.pun b/examples/struct.pun new file mode 100644 index 0000000..020d92d --- /dev/null +++ b/examples/struct.pun @@ -0,0 +1,44 @@ + +pkg main + +struct extra { + str note +} + +struct other { + str message + extra extra +} + +struct message { + i32 sender + i32 receiver + str body + other other +} + +fn send_message() { + message msg = message { + sender: 2, + receiver: 4, + body: "hello, world", + other: other { + message: "hello", + extra: extra { + note: "this is extra info", + }, + }, + } + + println(msg) + println(msg.sender) + println(msg.receiver) + println(msg.body) + println(msg.other) + println(msg.other.message) + println(msg.other.extra) + println(msg.other.extra.note) +} + +send_message() + diff --git a/examples/types.pun b/examples/types.pun new file mode 100644 index 0000000..9381c96 --- /dev/null +++ b/examples/types.pun @@ -0,0 +1,23 @@ +pkg main + +pub fn log_types() { + i32 c = 42 + i64 d = 42 + u32 g = 42 + u64 h = 42 + //f32 k = 42.0 + //f64 l = 42.0 + bool m = true + str n = "hello" + + println("i32:", c) + println("i64:", d) + println("u32:", g) + println("u64:", h) + //println("f32:", k) + //println("f64:", l) + println("bool:", m) + println("str:", n) +} + +log_types() diff --git a/examples/var.pun b/examples/var.pun new file mode 100644 index 0000000..cc03095 --- /dev/null +++ b/examples/var.pun @@ -0,0 +1,10 @@ +pkg main + +fn main() { + i32 a = 5 + i32 b = 10 + println("a =", a, "b =", b) + println("Sum =", a + b) +} + +main() diff --git a/internal/ast/statements.go b/internal/ast/statements.go index 4061229..9c64550 100644 --- a/internal/ast/statements.go +++ b/internal/ast/statements.go @@ -155,3 +155,34 @@ func (vd *VariableDeclaration) String() string { out.WriteString(";") return out.String() } + +type ForStatement struct { + Token token.Token + Init Statement + Condition Expression + Post Statement + Body *BlockStatement +} + +func (fs *ForStatement) statementNode() {} +func (fs *ForStatement) TokenLiteral() string { return fs.Token.Literal } +func (fs *ForStatement) String() string { + var out bytes.Buffer + + out.WriteString("for ") + if fs.Init != nil { + out.WriteString(fs.Init.String()) + } + out.WriteString("; ") + if fs.Condition != nil { + out.WriteString(fs.Condition.String()) + } + out.WriteString("; ") + if fs.Post != nil { + out.WriteString(fs.Post.String()) + } + out.WriteString(" ") + out.WriteString(fs.Body.String()) + + return out.String() +} diff --git a/internal/ast/struct.go b/internal/ast/struct.go index 34d3f8a..0a11cf9 100644 --- a/internal/ast/struct.go +++ b/internal/ast/struct.go @@ -85,3 +85,19 @@ func (sl *StructLiteral) String() string { out.WriteString(" }") return out.String() } + +type StructFieldAccess struct { + Token token.Token // The '.' token + Left Expression + Field *Identifier +} + +func (s *StructFieldAccess) expressionNode() {} + +func (s *StructFieldAccess) TokenLiteral() string { + return s.Token.Literal +} + +func (s *StructFieldAccess) String() string { + return s.Left.String() + "." + s.Field.String() +} diff --git a/internal/emitters/js/js.go b/internal/emitters/js/js.go new file mode 100644 index 0000000..8ac9215 --- /dev/null +++ b/internal/emitters/js/js.go @@ -0,0 +1,401 @@ +package js + +import ( + "bytes" + "fmt" + "strings" + + "github.com/dfirebaugh/punch/internal/ast" + "github.com/dfirebaugh/punch/internal/token" +) + +const ( + JSFunction = "function" + JSLet = "let" + JSReturn = "return" + JSIf = "if" + JSElse = "else" + JSClass = "class" + JSConstructor = "constructor" + JSNew = "new" + JSExport = "export" + JSConsoleLog = "console.log" + JSUnsupported = "// Unsupported" + JSFileComment = "// File: %s\n" + JSPackageComment = "// Package: %s\n\n" +) + +type Transpiler struct { + definedStructs map[string]bool +} + +func NewTranspiler() *Transpiler { + return &Transpiler{ + definedStructs: make(map[string]bool), + } +} + +func (t *Transpiler) Transpile(program *ast.Program) (string, error) { + var out bytes.Buffer + + for _, file := range program.Files { + out.WriteString(t.transpileFile(file)) + out.WriteString("\n") + } + + return out.String(), nil +} + +func (t *Transpiler) transpileFile(file *ast.File) string { + var out bytes.Buffer + var exports []string + + out.WriteString(fmt.Sprintf(JSFileComment, file.Filename)) + out.WriteString(fmt.Sprintf(JSPackageComment, file.PackageName)) + + for _, stmt := range file.Statements { + if functionStmt, ok := stmt.(*ast.FunctionStatement); ok && functionStmt.IsExported { + exports = append(exports, functionStmt.Name.String()) + } + out.WriteString(t.transpileStatement(stmt)) + out.WriteString("\n") + } + + if len(exports) > 0 { + out.WriteString("\n" + JSExport + " {\n") + for _, export := range exports { + out.WriteString(fmt.Sprintf(" %s,\n", export)) + } + out.WriteString("};\n") + } + + return out.String() +} + +func (t *Transpiler) transpileStatement(stmt ast.Statement) string { + switch stmt := stmt.(type) { + case *ast.ExpressionStatement: + return t.transpileExpression(stmt.Expression) + ";" + + case *ast.LetStatement: + return fmt.Sprintf("%s %s = %s;", JSLet, stmt.Name.String(), t.transpileExpression(stmt.Value)) + + case *ast.ReturnStatement: + return JSReturn + " " + t.transpileExpressions(stmt.ReturnValues) + ";" + + case *ast.FunctionStatement: + return t.transpileFunctionStatement(stmt) + + case *ast.IfStatement: + return t.transpileIfStatement(stmt) + + case *ast.BlockStatement: + return t.transpileBlockStatement(stmt) + + case *ast.StructDefinition: + return t.transpileStructDefinition(stmt) + + case *ast.VariableDeclaration: + return t.transpileVariableDeclaration(stmt) + + case *ast.ForStatement: + return t.transpileForStatement(stmt) + + default: + return JSUnsupported + " statement" + } +} + +func (t *Transpiler) transpileExpression(expr ast.Expression) string { + switch expr := expr.(type) { + case *ast.Identifier: + return expr.String() + + case *ast.IntegerLiteral: + return expr.String() + + case *ast.StringLiteral: + return fmt.Sprintf(`"%s"`, expr.String()) + + case *ast.BooleanLiteral: + return expr.String() + + case *ast.BinaryExpression: + return fmt.Sprintf("(%s %s %s)", + t.transpileExpression(expr.Left), + expr.Operator.Literal, + t.transpileExpression(expr.Right), + ) + + case *ast.CallExpression: + return t.transpileCallExpression(expr) + + case *ast.FunctionCall: + return t.transpileFunctionCall(expr) + + case *ast.IndexExpression: + return fmt.Sprintf("%s[%s]", + t.transpileExpression(expr.Left), + t.transpileExpression(expr.Index), + ) + + case *ast.PrefixExpression: + return fmt.Sprintf("(%s%s)", + expr.Operator.Literal, + t.transpileExpression(expr.Right), + ) + + case *ast.InfixExpression: + return fmt.Sprintf("(%s %s %s)", + t.transpileExpression(expr.Left), + expr.Operator.Literal, + t.transpileExpression(expr.Right), + ) + + case *ast.AssignmentExpression: + return t.transpileAssignmentExpression(expr) + + case *ast.FunctionLiteral: + return t.transpileFunctionLiteral(expr) + + case *ast.ArrayLiteral: + return t.transpileArrayLiteral(expr) + + case *ast.StructLiteral: + return t.transpileStructLiteral(expr) + + case *ast.StructFieldAccess: + return t.transpileStructFieldAccess(expr) + + default: + return JSUnsupported + " expression" + } +} + +func (t *Transpiler) transpileAssignmentExpression(expr *ast.AssignmentExpression) string { + return fmt.Sprintf("%s = %s", + t.transpileExpression(expr.Left), + t.transpileExpression(expr.Right), + ) +} + +func (t *Transpiler) transpileExpressions(exprs []ast.Expression) string { + var out []string + for _, expr := range exprs { + out = append(out, t.transpileExpression(expr)) + } + return strings.Join(out, ", ") +} + +func (t *Transpiler) transpileFunctionStatement(stmt *ast.FunctionStatement) string { + var out bytes.Buffer + out.WriteString(JSFunction + " ") + out.WriteString(stmt.Name.String()) + out.WriteString("(") + + params := []string{} + for _, param := range stmt.Parameters { + params = append(params, param.Identifier.Token.Literal) + } + out.WriteString(strings.Join(params, ", ")) + out.WriteString(") ") + + out.WriteString(t.transpileBlockStatement(stmt.Body)) + return out.String() +} + +func (t *Transpiler) transpileIfStatement(stmt *ast.IfStatement) string { + var out bytes.Buffer + + out.WriteString(JSIf + " (") + out.WriteString(t.transpileExpression(stmt.Condition)) + out.WriteString(") ") + out.WriteString(t.transpileBlockStatement(stmt.Consequence)) + + if stmt.Alternative != nil { + out.WriteString(" " + JSElse + " ") + out.WriteString(t.transpileBlockStatement(stmt.Alternative)) + } + + return out.String() +} + +func (t *Transpiler) transpileBlockStatement(stmt *ast.BlockStatement) string { + var out bytes.Buffer + + out.WriteString("{\n") + for _, s := range stmt.Statements { + out.WriteString(t.transpileStatement(s)) + out.WriteString("\n") + } + out.WriteString("}") + + return out.String() +} + +func (t *Transpiler) transpileFunctionCall(expr *ast.FunctionCall) string { + var out bytes.Buffer + + if expr.Function.String() == "println" { + out.WriteString(JSConsoleLog + "(") + } else { + out.WriteString(expr.Function.String() + "(") + } + + args := []string{} + for _, arg := range expr.Arguments { + args = append(args, t.transpileExpression(arg)) + } + out.WriteString(strings.Join(args, ", ")) + out.WriteString(")") + + return out.String() +} + +func (t *Transpiler) transpileCallExpression(expr *ast.CallExpression) string { + return t.transpileFunctionCall(&ast.FunctionCall{ + Function: expr.Function, + Arguments: expr.Arguments, + }) +} + +func (t *Transpiler) transpileFunctionLiteral(expr *ast.FunctionLiteral) string { + var out bytes.Buffer + + out.WriteString(JSFunction + "(") + params := []string{} + for _, param := range expr.Parameters { + params = append(params, param.String()) + } + out.WriteString(strings.Join(params, ", ")) + out.WriteString(") ") + out.WriteString(t.transpileBlockStatement(expr.Body)) + + return out.String() +} + +func (t *Transpiler) transpileArrayLiteral(expr *ast.ArrayLiteral) string { + var out bytes.Buffer + + out.WriteString("[") + elements := []string{} + for _, el := range expr.Elements { + elements = append(elements, t.transpileExpression(el)) + } + out.WriteString(strings.Join(elements, ", ")) + out.WriteString("]") + + return out.String() +} + +func (t *Transpiler) transpileStructDefinition(stmt *ast.StructDefinition) string { + var out bytes.Buffer + + if t.definedStructs == nil { + t.definedStructs = make(map[string]bool) + } + t.definedStructs[stmt.Name.String()] = true + + out.WriteString(JSClass + " ") + out.WriteString(stmt.Name.String()) + out.WriteString(" {\n") + out.WriteString(JSConstructor + "({") + fields := []string{} + for _, field := range stmt.Fields { + fields = append(fields, field.Name.String()) + } + out.WriteString(strings.Join(fields, ", ")) + out.WriteString("}) {\n") + for _, field := range stmt.Fields { + out.WriteString(fmt.Sprintf("this.%s = %s;\n", field.Name.String(), field.Name.String())) + } + out.WriteString("}\n") + out.WriteString("}") + + return out.String() +} + +func (t *Transpiler) transpileStructLiteral(expr *ast.StructLiteral) string { + var out bytes.Buffer + + out.WriteString(JSNew + " ") + out.WriteString(expr.StructName.String()) + out.WriteString("({") + fields := []string{} + for name, value := range expr.Fields { + fields = append(fields, fmt.Sprintf("%s: %s", name, t.transpileExpression(value))) + } + out.WriteString(strings.Join(fields, ", ")) + out.WriteString("})") + + return out.String() +} + +func (t *Transpiler) transpileStructFieldAccess(expr *ast.StructFieldAccess) string { + return fmt.Sprintf("%s.%s", + t.transpileExpression(expr.Left), + expr.Field.String(), + ) +} + +func (t *Transpiler) transpileVariableDeclaration(stmt *ast.VariableDeclaration) string { + if stmt.Type.Type == token.IDENTIFIER && t.definedStructs[stmt.Type.Literal] { + return fmt.Sprintf("const %s = new %s(%s);", + stmt.Name.String(), + stmt.Type.Literal, + t.transpileExpression(stmt.Value), + ) + } + if stmt.Type.Type == token.IDENTIFIER { + return fmt.Sprintf("%s = %s;", + stmt.Name.String(), + t.transpileExpression(stmt.Value), + ) + } + + return fmt.Sprintf("%s %s = %s;", + JSLet, + stmt.Name.String(), + t.transpileExpression(stmt.Value), + ) +} + +func (t *Transpiler) transpileForStatement(stmt *ast.ForStatement) string { + var out bytes.Buffer + + out.WriteString("for (") + + if stmt.Init != nil { + if letStmt, ok := stmt.Init.(*ast.VariableDeclaration); ok { + out.WriteString(fmt.Sprintf("%s %s = %s;", + JSLet, + letStmt.Name.String(), + t.transpileExpression(letStmt.Value), + )) + } else { + out.WriteString(t.transpileStatement(stmt.Init)) + } + } else { + out.WriteString(";") + } + + if stmt.Condition != nil { + out.WriteString(" " + t.transpileExpression(stmt.Condition) + ";") + } else { + out.WriteString(";") + } + + if stmt.Post != nil { + if letStmt, ok := stmt.Post.(*ast.VariableDeclaration); ok { + out.WriteString(" " + letStmt.Name.String() + " = " + t.transpileExpression(letStmt.Value)) + } else if exprStmt, ok := stmt.Post.(*ast.ExpressionStatement); ok { + out.WriteString(" " + t.transpileExpression(exprStmt.Expression)) + } else { + out.WriteString(" " + t.transpileStatement(stmt.Post)) + } + } + + out.WriteString(") ") + out.WriteString(t.transpileBlockStatement(stmt.Body)) + return out.String() +} diff --git a/internal/emitters/wat/function.go b/internal/emitters/wat/function.go index 3fc6b54..ce027fc 100644 --- a/internal/emitters/wat/function.go +++ b/internal/emitters/wat/function.go @@ -157,6 +157,12 @@ func collectExpressionLocals( *stringLiterals = append(*stringLiterals, strInit.String()) // *initializations = append(*initializations, fmt.Sprintf("(local.get $%s)\n", localVarName)) } + case *ast.StructLiteral: + for _, fieldValue := range e.Fields { + collectExpressionLocals(fieldValue, declaredLocals, locals, initializations, stringLiterals) + } + case *ast.StructFieldAccess: + collectExpressionLocals(e.Left, declaredLocals, locals, initializations, stringLiterals) } } @@ -164,15 +170,20 @@ func generateFunctionCall(call *ast.FunctionCall) string { var out strings.Builder if call.FunctionName == "println" && len(call.Arguments) > 0 { - strLiteral, ok := call.Arguments[0].(*ast.StringLiteral) - if !ok { - log.Fatal("println expects a string argument") - } - localVarName, exists := stringLiteralMap[strLiteral.Value] - if !exists { - log.Fatalf("String literal not found: %s", strLiteral.Value) + arg := call.Arguments[0] + if strLiteral, ok := arg.(*ast.StringLiteral); ok { + localVarName, exists := stringLiteralMap[strLiteral.Value] + if !exists { + log.Fatalf("String literal not found: %s", strLiteral.Value) + } + out.WriteString(fmt.Sprintf("(call $println (local.get $%s))\n", localVarName)) + } else if fieldAccess, ok := arg.(*ast.StructFieldAccess); ok { + out.WriteString(fmt.Sprintf("(call $println %s)\n", generateStructFieldAccess(fieldAccess))) + } else if ident, ok := arg.(*ast.Identifier); ok { + out.WriteString(fmt.Sprintf("(call $println (local.get $%s))\n", ident.Value)) + } else { + log.Fatal("println expects a string argument or struct field access") } - out.WriteString(fmt.Sprintf("(call $println (local.get $%s))\n", localVarName)) } else { out.WriteString(fmt.Sprintf("(call $%s ", call.FunctionName)) for _, arg := range call.Arguments { diff --git a/internal/emitters/wat/wat.go b/internal/emitters/wat/wat.go index fd4cb26..6d01719 100644 --- a/internal/emitters/wat/wat.go +++ b/internal/emitters/wat/wat.go @@ -14,7 +14,10 @@ const ( MemoryDeallocateFunc = "memory_deallocate" ) -var functionDeclarations map[string]*ast.FunctionDeclaration +var ( + functionDeclarations map[string]*ast.FunctionDeclaration + structDefinitions map[string]*ast.StructDefinition +) func findFunctionDeclarations(node ast.Node) { switch n := node.(type) { @@ -31,9 +34,26 @@ func findFunctionDeclarations(node ast.Node) { } } +func findStructDefinitions(node ast.Node) { + switch n := node.(type) { + case *ast.StructDefinition: + structDefinitions[n.Name.Value] = n + case *ast.BlockStatement: + for _, stmt := range n.Statements { + findStructDefinitions(stmt) + } + case *ast.Program: + for _, stmt := range n.Files[0].Statements { + findStructDefinitions(stmt) + } + } +} + func GenerateWAT(node ast.Node, withMemoryManagement bool) string { functionDeclarations = make(map[string]*ast.FunctionDeclaration) + structDefinitions = make(map[string]*ast.StructDefinition) findFunctionDeclarations(node) + findStructDefinitions(node) switch n := node.(type) { case *ast.Program: return generateStatements(n.Files[0].Statements, withMemoryManagement) @@ -102,6 +122,9 @@ func mapTypeToWAT(t string) string { case "f64": return "f64" default: + if _, ok := structDefinitions[t]; ok { + return "i32" + } log.Fatalf("Unsupported type: %s", t) return "" } @@ -400,6 +423,52 @@ func generateExpression(expr ast.Expression) string { out.WriteString(")\n") out.WriteString(")\n") return out.String() + case *ast.StructLiteral: + return generateStructLiteral(e) + case *ast.StructFieldAccess: + return generateStructFieldAccess(e) } return "" } + +func generateStructLiteral(lit *ast.StructLiteral) string { + structDef, ok := structDefinitions[lit.StructName.Value] + if !ok { + log.Fatalf("Undefined struct: %s", lit.StructName.Value) + } + + var out strings.Builder + structSize := len(structDef.Fields) * 4 + out.WriteString("(local $struct_ptr i32)\n") + out.WriteString(fmt.Sprintf("(local.set $struct_ptr (call $%s (i32.const %d)))\n", MemoryAllocateFunc, structSize)) + + for i, field := range structDef.Fields { + fieldValue, ok := lit.Fields[field.Name.Value] + if !ok { + log.Fatalf("Missing value for field: %s", field.Name.Value) + } + out.WriteString(fmt.Sprintf("(i32.store offset=%d (local.get $struct_ptr) %s)\n", i*4, generateExpression(fieldValue))) + } + + out.WriteString("(local.get $struct_ptr)\n") + return out.String() +} + +func generateStructFieldAccess(access *ast.StructFieldAccess) string { + structDef, ok := structDefinitions[access.Left.(*ast.Identifier).Value] + if !ok { + log.Fatalf("Undefined struct: %s", access.Left.(*ast.Identifier).Value) + } + + var fieldIndex int + for i, field := range structDef.Fields { + if field.Name.Value == access.Field.Value { + fieldIndex = i + break + } + } + + var out strings.Builder + out.WriteString(fmt.Sprintf("(i32.load offset=%d (local.get $%s))\n", fieldIndex*4, access.Left.(*ast.Identifier).Value)) + return out.String() +} diff --git a/internal/lexer/lexer.go b/internal/lexer/lexer.go index fdc084d..921038a 100644 --- a/internal/lexer/lexer.go +++ b/internal/lexer/lexer.go @@ -246,6 +246,8 @@ func (l Lexer) evaluateSpecialCharacter(literal string) token.Type { return token.RBRACE case token.BANG: return token.BANG + case token.MOD: + return token.MOD default: return token.ILLEGAL } @@ -275,6 +277,8 @@ func (l *Lexer) evaluateKeyword(literal string) token.Type { return token.F64 case token.STRING: return token.STRING + case token.Keywords[token.STRING]: + return token.STRING case token.Keywords[token.STRUCT]: return token.STRUCT case token.Keywords[token.INTERFACE]: @@ -297,6 +301,8 @@ func (l *Lexer) evaluateKeyword(literal string) token.Type { return token.IF case token.Keywords[token.TRUE]: return token.TRUE + case token.Keywords[token.FOR]: + return token.FOR case token.Keywords[token.FALSE]: return token.FALSE case token.Keywords[token.PUB]: diff --git a/internal/parser/errors.go b/internal/parser/errors.go index 7076455..c72aa02 100644 --- a/internal/parser/errors.go +++ b/internal/parser/errors.go @@ -8,9 +8,7 @@ import ( "github.com/sirupsen/logrus" ) -var ( - showFileName bool = false -) +var showFileName bool = false func init() { logrus.SetLevel(logrus.ErrorLevel) diff --git a/internal/parser/function.go b/internal/parser/function.go index bbf2715..cc76018 100644 --- a/internal/parser/function.go +++ b/internal/parser/function.go @@ -120,16 +120,9 @@ func (p *Parser) parseFunctionCall(function ast.Expression) ast.Expression { Token: p.curToken, Function: function, } - p.nextToken() + p.nextToken() // consume ( exp.Arguments = p.parseFunctionCallArguments() - if len(exp.Arguments) == 1 && p.peekTokenIs(token.RPAREN) { - p.nextToken() - } - if !p.expectCurrentTokenIs(token.RPAREN) { - p.error("expected ')' after function call arguments") - return nil - } p.trace("parsed function call after args", p.curToken.Literal, p.peekToken.Literal) return exp } @@ -142,20 +135,34 @@ func (p *Parser) parseFunctionCallArguments() []ast.Expression { p.trace("consume LPAREN", p.curToken.Literal, p.peekToken.Literal) } - args = append(args, p.parseExpression(LOWEST)) - p.trace("after parsing first arg", args[0].String(), p.curToken.Literal, p.peekToken.Literal) - if p.curTokenIs(token.RPAREN) { + p.nextToken() // consume the closing parenthesis return args } - for p.curTokenIs(token.COMMA) && !p.curTokenIs(token.RPAREN) { + firstArg := p.parseExpression(LOWEST) + if firstArg == nil { + p.error("could not parse first argument in function call") + return nil + } + p.trace("parseFunctionCallArguments: first arg", firstArg.String(), p.curToken.Literal, p.peekToken.Literal) + args = append(args, firstArg) + + for p.curTokenIs(token.COMMA) { + p.trace("parseFunctionCallArguments: consume COMMA", p.curToken.Literal, p.peekToken.Literal) + // consume the comma p.nextToken() - p.trace("func args 1", p.curToken.Literal, p.peekToken.Literal) - args = append(args, p.parseExpression(LOWEST)) - p.trace("func args 2", p.curToken.Literal, p.peekToken.Literal) + + nextArg := p.parseExpression(LOWEST) + if nextArg == nil { + p.error("could not parse argument after comma in function call") + return nil + } + p.trace("parseFunctionCallArguments: next arg", nextArg.String(), p.curToken.Literal, p.peekToken.Literal) + args = append(args, nextArg) } - p.trace("after parsing func args", p.curToken.Literal, p.peekToken.Literal) + + p.trace("parseFunctionCallArguments: end", p.curToken.Literal, p.peekToken.Literal) return args } diff --git a/internal/parser/parser.go b/internal/parser/parser.go index c3eb877..00f4f26 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -68,21 +68,23 @@ func (p *Parser) registerParseRules() { parseRules := map[token.Type]parseRule{ token.IDENTIFIER: {prefixFn: p.parseIdentifier}, token.STRING: {prefixFn: p.parseStringLiteral}, - // token.NUMBER: {prefixFn: p.parseIntegerLiteral}, - token.TRUE: {prefixFn: p.parseBooleanLiteral}, - token.FALSE: {prefixFn: p.parseBooleanLiteral}, - token.BANG: {prefixFn: p.parsePrefixExpression}, - token.ASSIGN: {infixFn: p.parseAssignmentExpression}, - token.MINUS: {infixFn: p.parseInfixExpression}, - token.PLUS: {infixFn: p.parseInfixExpression}, - token.ASTERISK: {infixFn: p.parseInfixExpression}, - token.SLASH: {infixFn: p.parseInfixExpression}, - token.EQ: {infixFn: p.parseInfixExpression}, - token.NOT_EQ: {infixFn: p.parseInfixExpression}, - token.LT: {infixFn: p.parseInfixExpression}, - token.GT: {infixFn: p.parseInfixExpression}, - token.AND: {infixFn: p.parseInfixExpression}, - token.OR: {infixFn: p.parseInfixExpression}, + token.TRUE: {prefixFn: p.parseBooleanLiteral}, + token.FALSE: {prefixFn: p.parseBooleanLiteral}, + token.BANG: {prefixFn: p.parsePrefixExpression}, + token.ASSIGN: {infixFn: p.parseAssignmentExpression}, + token.MINUS: {infixFn: p.parseInfixExpression}, + token.PLUS: {infixFn: p.parseInfixExpression}, + token.ASTERISK: {infixFn: p.parseInfixExpression}, + token.MOD: {infixFn: p.parseInfixExpression}, + token.SLASH: {infixFn: p.parseInfixExpression}, + token.EQ: {infixFn: p.parseInfixExpression}, + token.NOT_EQ: {infixFn: p.parseInfixExpression}, + token.LT_EQUALS: {infixFn: p.parseInfixExpression}, + token.GT_EQUALS: {infixFn: p.parseInfixExpression}, + token.LT: {infixFn: p.parseInfixExpression}, + token.GT: {infixFn: p.parseInfixExpression}, + token.AND: {infixFn: p.parseInfixExpression}, + token.OR: {infixFn: p.parseInfixExpression}, } numberTypes := []token.Type{ token.U8, token.U16, token.U32, token.U64, @@ -105,6 +107,8 @@ func (p *Parser) registerParseRules() { } func (p *Parser) parseExpression(precedence int) ast.Expression { + p.trace("parseExpression", p.curToken.Literal, p.peekToken.Literal) + if p.curToken.Literal == token.TRUE || p.curToken.Literal == token.FALSE { p.trace("parsing bool", p.curToken.Literal, p.peekToken.Literal) b := p.parseBooleanLiteral() @@ -112,9 +116,12 @@ func (p *Parser) parseExpression(precedence int) ast.Expression { p.peekToken.Type == token.PLUS || p.peekToken.Type == token.ASTERISK || p.peekToken.Type == token.SLASH || + p.peekToken.Type == token.MOD || p.peekToken.Type == token.EQ || p.peekToken.Type == token.NOT_EQ || p.peekToken.Type == token.LT || + p.peekToken.Type == token.LT_EQUALS || + p.peekToken.Type == token.GT_EQUALS || p.peekToken.Type == token.GT || p.peekToken.Type == token.AND || p.peekToken.Type == token.OR { @@ -148,10 +155,13 @@ func (p *Parser) parseExpression(precedence int) ast.Expression { p.peekToken.Type == token.PLUS || p.peekToken.Type == token.ASTERISK || p.peekToken.Type == token.SLASH || + p.peekToken.Type == token.MOD || p.peekToken.Type == token.EQ || p.peekToken.Type == token.NOT_EQ || p.peekToken.Type == token.LT || p.peekToken.Type == token.GT || + p.peekToken.Type == token.LT_EQUALS || + p.peekToken.Type == token.GT_EQUALS || p.peekToken.Type == token.AND || p.peekToken.Type == token.OR { p.trace("parsing infixed expression", p.curToken.Literal, p.peekToken.Literal) @@ -167,19 +177,28 @@ func (p *Parser) parseExpression(precedence int) ast.Expression { p.peekToken.Type == token.PLUS || p.peekToken.Type == token.ASTERISK || p.peekToken.Type == token.SLASH || + p.peekToken.Type == token.MOD || p.peekToken.Type == token.EQ || p.peekToken.Type == token.NOT_EQ || p.peekToken.Type == token.LT || p.peekToken.Type == token.GT || + p.peekToken.Type == token.LT_EQUALS || + p.peekToken.Type == token.GT_EQUALS || p.peekToken.Type == token.AND || p.peekToken.Type == token.OR { p.trace("parsing identifier infix expression", p.curToken.Literal, p.peekToken.Literal) ident := p.parseIdentifier() + if ident == nil { + p.error("identifier is nil") + } p.nextToken() return p.parseInfixExpression(ident) } if p.peekToken.Type == token.ASSIGN || p.peekToken.Type == token.INFER { ident := p.parseIdentifier() + if ident == nil { + p.error("identifier is nil") + } p.nextToken() return p.parseAssignmentExpression(ident) } @@ -190,6 +209,9 @@ func (p *Parser) parseExpression(precedence int) ast.Expression { if p.curTokenIs(token.IDENTIFIER) && p.peekTokenIs(token.LPAREN) { p.trace("parsing identifier functioncall expression", p.curToken.Literal, p.peekToken.Literal) ident := p.parseIdentifier() + if ident == nil { + p.error("identifier is nil") + } fnCall := p.parseFunctionCall(ident) p.trace("parsed identifier functioncall expression", p.curToken.Literal, p.peekToken.Literal) if p.curTokenIs(token.RPAREN) { @@ -197,7 +219,16 @@ func (p *Parser) parseExpression(precedence int) ast.Expression { } return fnCall } + ident := p.parseIdentifier() + if ident == nil { + p.error("identifier is nil") + } + + if p.peekTokenIs(token.DOT) { + return p.parseStructFieldAccess(ident.(*ast.Identifier)) + } + p.nextToken() return ident } @@ -228,9 +259,6 @@ func (p *Parser) parseExpression(precedence int) ast.Expression { func (p *Parser) nextToken() { p.curToken = p.peekToken p.peekToken = p.l.NextToken() - if p.curToken.Type == token.SEMICOLON { - p.nextToken() - } } // expectPeek checks if the next token is of the expected type. diff --git a/internal/parser/parsers.go b/internal/parser/parsers.go index 8d642ad..d8f5ff9 100644 --- a/internal/parser/parsers.go +++ b/internal/parser/parsers.go @@ -85,10 +85,6 @@ func (p *Parser) parseStatement() ast.Statement { return s } - if p.curTokenIs(token.IDENTIFIER) && p.peekTokenIs(token.INFER) { - return p.parseTypeBasedVariableDeclaration() - } - switch p.curToken.Type { case token.TRUE: return p.parseExpressionStatement() @@ -112,6 +108,8 @@ func (p *Parser) parseStatement() ast.Statement { return p.parseReturnStatement() case token.IF: return p.parseIfStatement() + case token.FOR: + return p.parseForStatement() case token.LBRACE: return p.parseBlockStatement() case token.IDENTIFIER: @@ -167,11 +165,6 @@ func (p *Parser) parseVariableDeclarationOrAssignment() ast.Statement { p.nextToken() value := p.parseExpression(LOWEST) - if !p.expectPeek(token.SEMICOLON) { - p.error("expected ';' after expression") - return nil - } - return &ast.VariableDeclaration{ Type: typeToken, Name: name.(*ast.Identifier), @@ -185,9 +178,6 @@ func (p *Parser) parseExpressionStatement() *ast.ExpressionStatement { if stmt.Expression != nil { p.trace("after parsing expression statement", stmt.Expression.String()) } - if p.peekTokenIs(token.SEMICOLON) { - p.nextToken() - } return stmt } @@ -200,10 +190,10 @@ func (p *Parser) parseNumberType() ast.Expression { } return &ast.IntegerLiteral{ Token: token.Token{ - Type: token.I64, - Literal: p.curToken.Literal, - Position: p.curToken.Position, - }, + Type: token.I64, + Literal: p.curToken.Literal, + Position: p.curToken.Position, + }, Value: int64(d), } } @@ -222,7 +212,13 @@ func (p *Parser) parseFloatType() ast.Expression { } func (p *Parser) parseIdentifier() ast.Expression { - return &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} + ident := &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} + + if p.peekTokenIs(token.DOT) { + return p.parseStructFieldAccess(ident) + } + + return ident } func (p *Parser) parseIntegerLiteral() ast.Expression { @@ -351,16 +347,13 @@ func (p *Parser) parseBlockStatement() *ast.BlockStatement { // parseComment skips over a comment token and moves the parser to the end of the comment. func (p *Parser) parseComment() { - if p.curToken.Type != token.SLASH_SLASH && p.curToken.Type != token.SLASH_ASTERISK { - return - } - - if p.curToken.Type == token.SLASH_SLASH { + switch p.curToken.Type { + case token.SLASH_SLASH: currentLine := p.curToken.Position.Line for p.curToken.Position.Line == currentLine { p.nextToken() } - } else if p.curToken.Type == token.SLASH_ASTERISK { + case token.SLASH_ASTERISK: // Advance past the opening token p.nextToken() @@ -374,6 +367,8 @@ func (p *Parser) parseComment() { // Advance past the closing token p.nextToken() + default: + return } } @@ -461,3 +456,75 @@ func (p *Parser) inferType(value ast.Expression) token.Type { return token.ILLEGAL } } + +func (p *Parser) parseForStatement() *ast.ForStatement { + p.trace("parsing for statement", p.curToken.Literal, p.peekToken.Literal) + + // Create a ForStatement node in your AST + stmt := &ast.ForStatement{Token: p.curToken} + + // Consume the "for" token so p.curToken is now what's after "for" + p.nextToken() + + // -- Step 1: Parse the init statement -------------------------------- + // e.g., "i32 i = 1;" + stmt.Init = p.parseStatement() + if stmt.Init == nil { + p.error("expected initialization statement in for loop") + return nil + } + + p.trace("current token", p.curToken.Literal) + + // After parsing the init statement, we must see a semicolon. + // For example, "i32 i = 1;" must end with ';' + if !p.curTokenIs(token.SEMICOLON) { + p.error("expected ';' after for-init statement") + return nil + } + // Consume the ';' + p.nextToken() + + p.trace("current token", p.curToken.Literal) + // -- Step 2: Parse the condition ------------------------------------- + // e.g., "i <= n;" + // Condition can be empty in Go-like syntax, but your grammar might require it. + // We'll parse an expression if we don't see another semicolon right away. + if !p.curTokenIs(token.SEMICOLON) { + stmt.Condition = p.parseExpression(LOWEST) + if stmt.Condition == nil { + p.error("expected condition expression in for loop") + return nil + } + } + + p.trace("current token", p.curToken.Literal) + if !p.curTokenIs(token.SEMICOLON) { + p.error("expected ';' after for-loop condition") + return nil + } + // consume the second ';' + p.nextToken() + + // -- Step 3: Parse the post statement -------------------------------- + // e.g., "i = i + 1" + // This can also be empty in many β€œGo-like” syntaxes, but typically you have something. + if !p.curTokenIs(token.LBRACE) { + stmt.Post = p.parseStatement() + } + + // -- Step 4: Finally, we expect '{' to parse the for-block ----------- + if !p.curTokenIs(token.LBRACE) { + p.error("expected '{' after for loop post statement") + return nil + } + + // parseBlockStatement parses everything in { ... } until matching '}' + stmt.Body = p.parseBlockStatement() + + if !p.curTokenIs(token.LBRACE) { + p.nextToken() + } + + return stmt +} diff --git a/internal/parser/struct.go b/internal/parser/struct.go index fd9ae64..95b1c87 100644 --- a/internal/parser/struct.go +++ b/internal/parser/struct.go @@ -60,6 +60,7 @@ func (p *Parser) parseStructFields() []*ast.StructField { p.nextToken() field := p.parseStructField() if field == nil { + p.error("field is nil") return nil } fields = append(fields, field) @@ -79,6 +80,10 @@ func (p *Parser) parseStructLiteral() ast.Expression { structLit := &ast.StructLiteral{ Token: p.curToken, Fields: make(map[string]ast.Expression), + StructName: &ast.Identifier{ + Token: p.curToken, + Value: p.curToken.Literal, + }, } p.nextToken() @@ -124,5 +129,29 @@ func (p *Parser) parseStructLiteral() ast.Expression { p.nextToken() } + p.nextToken() return structLit } + +func (p *Parser) parseStructFieldAccess(left ast.Expression) ast.Expression { + p.nextToken() // consume the dot + + if !p.expectPeek(token.IDENTIFIER) { + p.error("expected identifier after dot operator") + return nil + } + + fieldAccess := &ast.StructFieldAccess{ + Token: p.curToken, // the dot + Left: left, + Field: &ast.Identifier{Token: p.peekToken, Value: p.peekToken.Literal}, + } + + p.nextToken() // consume the field identifier + + if p.peekTokenIs(token.DOT) { + return p.parseStructFieldAccess(fieldAccess) + } + + return fieldAccess +} diff --git a/internal/token/token.go b/internal/token/token.go index b59e1b9..10823a5 100644 --- a/internal/token/token.go +++ b/internal/token/token.go @@ -24,7 +24,7 @@ const ( EOF = "EOF" // Literals - STRING = "string" + STRING = "STRING" NUMBER = "number" FLOAT = "float" BOOL = "bool" @@ -75,6 +75,7 @@ const ( CONST = "CONST" LET = "LET" RETURN = "RETURN" + FOR = "FOR" IF = "if" ELSE = "else" PUB = "pub" @@ -120,7 +121,9 @@ var Keywords = map[Type]string{ LET: "let", IF: "if", ELSE: "else", + FOR: "for", TRUE: "true", FALSE: "false", PUB: "pub", + STRING: "str", } diff --git a/scripts/build_wasm.sh b/scripts/build_wasm.sh new file mode 100644 index 0000000..3eccf23 --- /dev/null +++ b/scripts/build_wasm.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cp $(go env GOROOT)/misc/wasm/wasm_exec.js ./tools/ast_explorer/static/ +GOOS=js GOARCH=wasm go build -o ./tools/ast_explorer/static/main.wasm ./tools/punchgen/ diff --git a/scripts/deploy_gh_pages.sh b/scripts/deploy_gh_pages.sh new file mode 100644 index 0000000..7239b4b --- /dev/null +++ b/scripts/deploy_gh_pages.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +source ./scripts/build_wasm.sh + +GIT_REPO_URL=$(git config --get remote.origin.url) + +cd ./tools/ast_explorer/static/ +git init . +git remote add github $GIT_REPO_URL +git checkout -b gh-pages +git add . +git commit -am "gh-pages deploy" +git push github gh-pages --force +cd ../.. diff --git a/tools/ast_explorer/main.go b/tools/ast_explorer/main.go index 2b60a60..34a8c38 100644 --- a/tools/ast_explorer/main.go +++ b/tools/ast_explorer/main.go @@ -1,21 +1,18 @@ package main import ( - "embed" "encoding/json" "fmt" - "io/fs" "net/http" + "os" + "github.com/dfirebaugh/punch/internal/emitters/js" "github.com/dfirebaugh/punch/internal/emitters/wat" "github.com/dfirebaugh/punch/internal/lexer" "github.com/dfirebaugh/punch/internal/parser" "github.com/dfirebaugh/punch/internal/token" ) -//go:embed static/* -var staticFiles embed.FS - func parseHandler(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { @@ -115,18 +112,56 @@ func watHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte(watCode)) } -func main() { - publicFS, err := fs.Sub(staticFiles, "static") +func jsHandler(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + errMessage := fmt.Sprintf("An error occurred: %v", rec) + http.Error(w, errMessage, http.StatusInternalServerError) + } + }() + + if r.Method != http.MethodPost { + http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed) + return + } + + var requestBody struct { + Source string `json:"source"` + } + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + l := lexer.New("example", requestBody.Source) + p := parser.New(l) + + program := p.ParseProgram("ast_explorer") + + t := js.NewTranspiler() + jsCode, err := t.Transpile(program) if err != nil { - panic(fmt.Errorf("failed to get subdirectory: %w", err)) + http.Error(w, "Failed to transpile to JS", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/javascript") + w.Write([]byte(jsCode)) +} + +func main() { + staticDir := "./tools/ast_explorer/static" + if _, err := os.Stat(staticDir); os.IsNotExist(err) { + panic(fmt.Errorf("static directory does not exist: %w", err)) } - fileServer := http.FileServer(http.FS(publicFS)) + fileServer := http.FileServer(http.Dir(staticDir)) http.Handle("/", fileServer) http.HandleFunc("/parse", parseHandler) http.HandleFunc("/lex", lexHandler) http.HandleFunc("/wat", watHandler) + http.HandleFunc("/js", jsHandler) fmt.Println("Server running on http://localhost:8080") http.ListenAndServe(":8080", nil) diff --git a/tools/ast_explorer/static/editor.js b/tools/ast_explorer/static/editor.js index b8205b1..dda83ed 100644 --- a/tools/ast_explorer/static/editor.js +++ b/tools/ast_explorer/static/editor.js @@ -1,4 +1,4 @@ -import { renderJSON } from "/ast.js"; +import { renderJSON } from "./ast.js"; export const editor = CodeMirror(document.getElementById("editor"), { mode: "rust", @@ -9,20 +9,22 @@ export const editor = CodeMirror(document.getElementById("editor"), { pkg main bool is_eq(i32 a, i32 b) { - return a == b + return a == b } -pub i32 add_two(i32 x, i32 y, i32 z) { - println("x = {}, y = {}", x, y, z) - println("Hello, World!") - return x + y +pub i32 add_two(i32 x, i32 y) { + println("x =", x, "y =", y) + println("Hello, World!") + return x + y } pub i32 add_four(i32 a, i32 b, i32 c, i32 d) { - return a + b + c + d + return a + b + c + d } - `.trim(), +println(add_two(2, 5)) + + `.trim(), }); const highlightCode = (startLine, startCol, endLine, endCol) => { @@ -42,9 +44,9 @@ export const fetchAndRenderAST = () => { }, body: JSON.stringify({ source }), }) - .then((response) => { + .then(async (response) => { if (!response.ok) { - throw new Error(`Server error: ${response.statusText}`); + throw new Error(`Server error: ${await response.text()}`); } return response.json(); }) diff --git a/tools/ast_explorer/static/index.html b/tools/ast_explorer/static/index.html index 4ad7677..4924747 100644 --- a/tools/ast_explorer/static/index.html +++ b/tools/ast_explorer/static/index.html @@ -3,7 +3,7 @@ - AST Explorer + punch lang + + + - - - + +
+
+ + + +
- + +
-
+
+
diff --git a/tools/ast_explorer/static/index.js b/tools/ast_explorer/static/index.js index 05b1137..38c81a1 100644 --- a/tools/ast_explorer/static/index.js +++ b/tools/ast_explorer/static/index.js @@ -1,113 +1,498 @@ -import { editor, fetchAndRenderAST } from "/editor.js"; +import { editor } from "./editor.js"; +import { renderJSON } from "./ast.js"; -const resizer = document.getElementById("resizer"); -const editorContainer = document.querySelector(".editor-container"); -const outputContainer = document.querySelector(".output-container"); +const snippets = { + example: ` +pkg main -let isResizing = false; +bool is_eq(i32 a, i32 b) { + return a == b +} -resizer.addEventListener("mousedown", (e) => { - isResizing = true; - document.body.style.cursor = "col-resize"; -}); +pub i32 add_two(i32 x, i32 y) { + println("x =", x, "y =", y) + println("Hello, World!") + return x + y +} -document.addEventListener("mousemove", (e) => { - if (!isResizing) return; +println(add_two(2, 5)) + `.trim(), + multiply: ` +pkg main - const containerRect = document - .querySelector(".container") - .getBoundingClientRect(); - const newEditorWidth = e.clientX - containerRect.left; - const newOutputWidth = - containerRect.width - newEditorWidth - resizer.offsetWidth; +pub i32 multiply(i32 a, i32 b) { + return a * b +} - if (newEditorWidth > 100 && newOutputWidth > 100) { - editorContainer.style.width = `${newEditorWidth}px`; - outputContainer.style.width = `${newOutputWidth}px`; - } -}); +println(multiply(3, 4)) + `.trim(), + greet: ` +pkg main -document.addEventListener("mouseup", () => { - isResizing = false; - document.body.style.cursor = "default"; -}); +pub fn greet(str name) { + println("Hello,", name) +} + +greet("World!") + `.trim(), + math: ` +pkg main -function parseCode() { - switchTab("ast"); - fetchAndRenderAST(); +pub fn math_operations() { + i32 a = 10 + i32 b = 20 + println("Addition: ", a + b) + println("Subtraction: ", a - b) + println("Multiplication: ", a * b) + println("Division: ", b / a) + println("Modulus: ", b % a) } -document.getElementById("tab-ast").addEventListener("click", () => { - parseCode(); -}); -parseCode(); - -document.getElementById("tab-lex").addEventListener("click", () => { - switchTab("lex"); - const source = editor.getValue().trim(); - - fetch("/lex", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ source }), - }) - .then((response) => { - if (!response.ok) { - throw new Error(`Server error: ${response.statusText}`); - } - return response.json(); - }) - .then((tokens) => { - const outputElement = document.getElementById("output"); - outputElement.innerHTML = `
${JSON.stringify(tokens, null, 2)}
`; - }) - .catch((error) => { - const outputElement = document.getElementById("output"); - outputElement.innerText = `Error: ${error.message}`; - }); -}); -document.getElementById("tab-wat").addEventListener("click", () => { - switchTab("wat"); - const source = editor.getValue().trim(); - - fetch("/wat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ source }), - }) - .then((response) => { - if (!response.ok) { - throw new Error(`Server error: ${response.statusText}`); - } - return response.text(); - }) - .then((watCode) => { - const outputElement = document.getElementById("output"); +math_operations() + `.trim(), + loop: ` +pkg main + +pub fn count_to(i32 n) { + for i32 i = 1; i <= n; i = i + 1 { + println(i) + } +} + +count_to(5) + `.trim(), + "return": ` +pkg main + +i32 square(i32 n) { + return n * n +} + +println("Square of 4 is", square(4)) + `.trim(), + types: ` +pkg main + +pub fn log_types() { + i32 c = 42 + i64 d = 42 + u32 g = 42 + u64 h = 42 + //f32 k = 42.0 + //f64 l = 42.0 + bool m = true + str n = "hello" + + println("i32:", c) + println("i64:", d) + println("u32:", g) + println("u64:", h) + //println("f32:", k) + //println("f64:", l) + println("bool:", m) + println("str:", n) +} + +log_types() + `.trim(), + struct: ` +pkg main + +struct extra { + str note +} + +struct other { + str message + extra extra +} + +struct message { + i32 sender + i32 receiver + str body + other other +} + +fn send_message() { + message msg = message { + sender: 2, + receiver: 4, + body: "hello, world", + other: other { + message: "hello", + extra: extra { + note: "this is extra info", + }, + }, + } + + println(msg) + println(msg.sender) + println(msg.receiver) + println(msg.body) + println(msg.other) + println(msg.other.message) + println(msg.other.extra) + println(msg.other.extra.note) +} + +send_message() + + `.trim(), +}; + +document.addEventListener("DOMContentLoaded", () => { + const resizer = document.getElementById("resizer"); + const editorContainer = document.querySelector(".editor-container"); + const outputContainer = document.querySelector(".output-container"); + + let isResizing = false; + let activeTab = localStorage.getItem("activeTab") || "js"; + let goInstance; + let wasmInstance; + let wasmRunning = false; + let vimModeEnabled = localStorage.getItem("vimModeEnabled") === "true"; + + resizer.addEventListener("mousedown", (e) => { + isResizing = true; + document.body.style.cursor = "col-resize"; + }); + + document.addEventListener("mousemove", (e) => { + if (!isResizing) return; + + const containerRect = document + .querySelector(".container") + .getBoundingClientRect(); + const newEditorWidth = e.clientX - containerRect.left; + const newOutputWidth = + containerRect.width - newEditorWidth - resizer.offsetWidth; + + if (newEditorWidth > 100 && newOutputWidth > 100) { + editorContainer.style.width = `${newEditorWidth}px`; + outputContainer.style.width = `${newOutputWidth}px`; + } + }); + + document.addEventListener("mouseup", () => { + isResizing = false; + document.body.style.cursor = "default"; + }); + + async function initializeWasm() { + if (wasmRunning) { + console.warn("WASM instance already running"); + return; + } + goInstance = new Go(); + const result = await WebAssembly.instantiateStreaming( + fetch("main.wasm"), + goInstance.importObject, + ); + wasmInstance = result.instance; + goInstance.run(wasmInstance); + wasmRunning = true; + } + + async function ensureWasmRunning() { + if (!wasmRunning) { + await initializeWasm(); + } + } + + function handleWasmError(error) { + console.error("WASM error:", error); + wasmRunning = false; + const outputElement = document.getElementById("output"); + outputElement.innerText = "An error occurred, failed to generate"; + initializeWasm() + .then(() => { + console.log("WASM instance restarted"); + }) + .catch((initError) => { + console.error("Failed to restart WASM instance:", initError); + }); + } + + function parseCode() { + ensureWasmRunning() + .then(() => { + try { + switchTab("ast"); + const source = editor.getValue().trim(); + const ast = parse(source); + const outputElement = document.getElementById("output"); + outputElement.innerHTML = ""; + renderJSON(JSON.parse(ast), outputElement); + } catch (error) { + handleWasmError(error); + } + }) + .catch((error) => { + console.error("Failed to parse code:", error); + }); + } + + document.getElementById("tab-ast").addEventListener("click", () => { + activeTab = "ast"; + localStorage.setItem("activeTab", activeTab); + parseCode(); + }); + + document.getElementById("tab-lex").addEventListener("click", () => { + activeTab = "lex"; + localStorage.setItem("activeTab", activeTab); + ensureWasmRunning() + .then(() => { + try { + switchTab("lex"); + const source = editor.getValue().trim(); + const tokens = lex(source); + const outputElement = document.getElementById("output"); + outputElement.innerHTML = `
${JSON.parse(tokens).join("\n")}
`; + } catch (error) { + handleWasmError(error); + } + }) + .catch((error) => { + console.error("Failed to lex code:", error); + }); + }); + + document.getElementById("tab-wat").addEventListener("click", () => { + activeTab = "wat"; + localStorage.setItem("activeTab", activeTab); + ensureWasmRunning() + .then(() => { + try { + switchTab("wat"); + const source = editor.getValue().trim(); + const watCode = generateWAT(source); + const outputElement = document.getElementById("output"); + outputElement.innerHTML = ""; + const watEditor = CodeMirror(outputElement, { + value: watCode, + mode: "wat", + lineNumbers: true, + theme: "dracula", + readOnly: true, + }); + } catch (error) { + handleWasmError(error); + } + }) + .catch((error) => { + console.error("Failed to generate WAT code:", error); + }); + }); + + document.getElementById("tab-js").addEventListener("click", () => { + activeTab = "js"; + localStorage.setItem("activeTab", activeTab); + ensureWasmRunning() + .then(() => { + try { + switchTab("js"); + const source = editor.getValue().trim(); + const jsCode = generateJS(source); + const formattedJsCode = prettier.format(jsCode, { + parser: "babel", + plugins: prettierPlugins, + }); + const outputElement = document.getElementById("output"); + outputElement.classList.add("CodeMirror-js"); + outputElement.innerHTML = ""; + const jsEditor = CodeMirror(outputElement, { + value: formattedJsCode, + mode: "javascript", + lineNumbers: true, + theme: "dracula", + readOnly: true, + }); + + document.getElementById("js-console").style.display = "block"; + } catch (error) { + handleWasmError(error); + } + }) + .catch((error) => { + console.error("Failed to generate JS code:", error); + }); + }); + + function switchTab(tab) { + document + .querySelectorAll(".tab") + .forEach((t) => t.classList.remove("active")); + document.getElementById(`tab-${tab}`).classList.add("active"); + + const outputElement = document.getElementById("output"); + if (tab !== "ast") { outputElement.innerHTML = ""; - const watEditor = CodeMirror(outputElement, { - value: watCode, - mode: "wat", - lineNumbers: true, - theme: "dracula", - readOnly: true, + } + + if (tab !== "js") { + document.getElementById("js-console").style.display = "none"; + } + } + + function fetchAndRenderAST() { + ensureWasmRunning() + .then(() => { + try { + const source = editor.getValue().trim(); + + switch (activeTab) { + case "ast": + const ast = parse(source); + const outputElement = document.getElementById("output"); + outputElement.innerHTML = ""; + renderJSON(JSON.parse(ast), outputElement); + break; + case "lex": + const tokens = lex(source); + const outputElementLex = document.getElementById("output"); + outputElementLex.innerHTML = `
${tokens}
`; + break; + case "wat": + const watCode = generateWAT(source); + const outputElementWat = document.getElementById("output"); + outputElementWat.innerHTML = ""; + const watEditor = CodeMirror(outputElementWat, { + value: watCode, + mode: "wat", + lineNumbers: true, + theme: "dracula", + readOnly: true, + }); + break; + case "js": + const jsCode = generateJS(source); + const formattedJsCode = prettier.format(jsCode, { + parser: "babel", + plugins: prettierPlugins, + }); + const outputElementJs = document.getElementById("output"); + outputElementJs.innerHTML = ""; + const jsEditor = CodeMirror(outputElementJs, { + value: formattedJsCode, + mode: "javascript", + lineNumbers: true, + theme: "dracula", + readOnly: true, + }); + + document.getElementById("js-console").style.display = "block"; + break; + default: + console.error("Unknown tab:", activeTab); + } + } catch (error) { + handleWasmError(error); + } + }) + .catch((error) => { + console.error("Failed to fetch and render AST:", error); }); + } + + // Listen for :w command in Vim mode + CodeMirror.Vim.defineEx("write", "w", () => { + fetchAndRenderAST(); + }); + + initializeWasm() + .then(() => { + document.getElementById(`tab-${activeTab}`).click(); }) .catch((error) => { - const outputElement = document.getElementById("output"); - outputElement.innerText = `Error: ${error.message}`; + console.error("Failed to initialize WASM:", error); }); -}); -function switchTab(tab) { - document - .querySelectorAll(".tab") - .forEach((t) => t.classList.remove("active")); - document.getElementById(`tab-${tab}`).classList.add("active"); + document.getElementById("run-js").addEventListener("click", () => { + const outputElement = document.getElementById("output"); + const jsCode = outputElement + .querySelector(".CodeMirror") + .CodeMirror.getValue(); + const consoleOutput = document.getElementById("console-output"); - const outputElement = document.getElementById("output"); - if (tab !== "ast") { - outputElement.innerHTML = ""; // Clear output for lexed tokens and WAT code - } -} + consoleOutput.textContent = ""; + + const originalConsoleLog = console.log; + const originalConsoleError = console.error; + console.log = (...args) => { + originalConsoleLog(...args); + consoleOutput.textContent += + args + .map((arg) => + typeof arg === "object" ? JSON.stringify(arg, null, 2) : arg, + ) + .join(" ") + "\n"; + }; + console.error = (...args) => { + originalConsoleError(...args); + consoleOutput.textContent += `Error: ${args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg, null, 2) : arg)).join(" ")}\n`; + }; + + try { + const blob = new Blob([jsCode], { type: "application/javascript" }); + const url = URL.createObjectURL(blob); + + import(url) + .then((module) => { + URL.revokeObjectURL(url); + console.log = originalConsoleLog; + console.error = originalConsoleError; + }) + .catch((error) => { + console.error(`Error: ${error.message}`); + URL.revokeObjectURL(url); + console.log = originalConsoleLog; + console.error = originalConsoleError; + }); + } catch (error) { + console.log = originalConsoleLog; + console.error = originalConsoleError; + console.error(`Error: ${error.message}`); + } + }); + + document.getElementById("clear-console").addEventListener("click", () => { + const consoleOutput = document.getElementById("console-output"); + consoleOutput.textContent = ""; + }); -document.getElementById("tab-wat").click(); + document.getElementById("compile-code").addEventListener("click", () => { + fetchAndRenderAST(); + }); + + document.getElementById("toggle-vim").addEventListener("click", () => { + vimModeEnabled = !vimModeEnabled; + localStorage.setItem("vimModeEnabled", vimModeEnabled); + editor.setOption("keyMap", vimModeEnabled ? "vim" : "default"); + document.getElementById("toggle-vim").textContent = vimModeEnabled + ? "Disable Vim Mode" + : "Enable Vim Mode"; + }); + + editor.setOption("keyMap", vimModeEnabled ? "vim" : "default"); + document.getElementById("toggle-vim").textContent = vimModeEnabled + ? "Disable Vim Mode" + : "Enable Vim Mode"; + + const snippetSelect = document.getElementById("code-snippets"); + Object.keys(snippets).forEach((key) => { + const option = document.createElement("option"); + option.value = key; + option.textContent = key.replace("snippet", "Snippet "); + snippetSelect.appendChild(option); + }); + + snippetSelect.addEventListener("change", (event) => { + const snippetKey = event.target.value; + if (snippets[snippetKey]) { + editor.setValue(snippets[snippetKey]); + } + }); +}); diff --git a/tools/ast_explorer/static/style.css b/tools/ast_explorer/static/style.css new file mode 100644 index 0000000..1091e38 --- /dev/null +++ b/tools/ast_explorer/static/style.css @@ -0,0 +1,150 @@ +body { + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + height: 100vh; + margin: 0; +} + +h1 { + text-align: center; + margin: 10px 0; + color: #f8f8f2; +} + +.container { + display: grid; + grid-template-columns: 1fr 5px 1fr; + grid-template-rows: 1fr; + height: 100%; +} + +.editor-container { + background-color: #282a36; + padding: 10px; + overflow: auto; +} + +.output-container { + background-color: #2b2b2b; + color: #f8f8f2; + font-family: "Courier New", Courier, monospace; + font-size: 14px; + line-height: 1.5; + user-select: none; + overflow: auto; +} + +.resizer { + background-color: #444; + cursor: col-resize; + width: 5px; + z-index: 1; +} + +.editor { + height: 100%; +} + +.json-key { + font-weight: bold; + color: #66d9ef; + cursor: pointer; +} + +.json-key:hover { + text-decoration: underline; +} + +.json-value { + color: #a6e22e; + margin-left: 10px; +} + +.json-bracket { + color: #f92672; +} + +.collapsed > .json-value { + display: none; +} + +.toggle-button { + margin-left: 5px; + padding: 2px 5px; + font-size: 10px; + color: #fff; + background-color: rgba(0, 0, 0, 0); + border: none; + cursor: pointer; + border-radius: 3px; +} + +.CodeMirror .highlighted { + background-color: yellow; + border-bottom: 2px solid orange; +} +.CodeMirror { + height: 100vh; +} + +.CodeMirror-js .CodeMirror { + height: 70vh; +} + +.tabs { + display: flex; + background-color: #333; + height: 2rem; +} + +.tab { + flex: 1; + padding: 10px; + text-align: center; + color: #fff; + background-color: #444; + cursor: pointer; + border: none; + outline: none; +} + +.tab.active { + background-color: #666; + font-weight: bold; +} + +.editor-buttons { + margin-top: 5px; + margin-bottom: 5px; +} + +.editor-buttons button { + margin-right: 10px; + padding: 5px 10px; + font-size: 14px; + color: #fff; + background-color: #444; + border: none; + cursor: pointer; + border-radius: 3px; +} + +.editor-buttons button:hover { + background-color: #666; +} + +.editor-buttons select { + margin-right: 10px; + padding: 5px 10px; + font-size: 14px; + color: #fff; + background-color: #444; + border: none; + cursor: pointer; + border-radius: 3px; +} + +.editor-buttons select:hover { + background-color: #666; +} diff --git a/tools/punchgen/main.go b/tools/punchgen/main.go new file mode 100644 index 0000000..7575faf --- /dev/null +++ b/tools/punchgen/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "encoding/json" + "fmt" + "syscall/js" + + js_gen "github.com/dfirebaugh/punch/internal/emitters/js" + "github.com/dfirebaugh/punch/internal/emitters/wat" + "github.com/dfirebaugh/punch/internal/lexer" + "github.com/dfirebaugh/punch/internal/parser" + "github.com/dfirebaugh/punch/internal/token" +) + +func parse(this js.Value, p []js.Value) interface{} { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered in parse:", r) + } + }() + source := p[0].String() + l := lexer.New("example", source) + parser := parser.New(l) + program := parser.ParseProgram("ast_explorer") + + astJSON, err := json.MarshalIndent(program, "", " ") + if err != nil { + return map[string]interface{}{ + "error": fmt.Sprintf("Failed to generate AST JSON: %v", err), + } + } + + return string(astJSON) +} + +func lex(this js.Value, p []js.Value) interface{} { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered in lex:", r) + } + }() + source := p[0].String() + l := lexer.New("example", source) + var tokens []string + for tok := l.NextToken(); tok.Type != token.EOF; tok = l.NextToken() { + tokens = append(tokens, fmt.Sprintf("%s: %q", tok.Type, tok.Literal)) + } + + tokensJSON, err := json.Marshal(tokens) + if err != nil { + return map[string]interface{}{ + "error": fmt.Sprintf("Failed to generate tokens JSON: %v", err), + } + } + + return string(tokensJSON) +} + +func generateWAT(this js.Value, p []js.Value) interface{} { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered in generateWAT:", r) + } + }() + source := p[0].String() + l := lexer.New("example", source) + parser := parser.New(l) + program := parser.ParseProgram("ast_explorer") + + watCode := wat.GenerateWAT(program, true) + return watCode +} + +func generateJS(this js.Value, p []js.Value) interface{} { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered in generateJS:", r) + } + }() + source := p[0].String() + l := lexer.New("example", source) + parser := parser.New(l) + program := parser.ParseProgram("ast_explorer") + + t := js_gen.NewTranspiler() + jsCode, err := t.Transpile(program) + if err != nil { + return map[string]interface{}{ + "error": fmt.Sprintf("Failed to transpile to JS: %v", err), + } + } + + return jsCode +} + +func main() { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered in main:", r) + } + }() + js.Global().Set("parse", js.FuncOf(parse)) + js.Global().Set("lex", js.FuncOf(lex)) + js.Global().Set("generateWAT", js.FuncOf(generateWAT)) + js.Global().Set("generateJS", js.FuncOf(generateJS)) + + select {} +}