diff --git a/.gitignore b/.gitignore index 036b621..112122e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ # Roblox Studio lock files /*.rbxlx.lock -/*.rbxl.lock \ No newline at end of file +/*.rbxl.lock + +/Sync/Server/tools diff --git a/Luau/main.go b/Luau/main.go index 86e1f74..6c8b368 100644 --- a/Luau/main.go +++ b/Luau/main.go @@ -4,6 +4,7 @@ import ( luau "Luau/binding" "context" "fmt" + "math/rand" "os" "strings" @@ -11,6 +12,15 @@ import ( sitter "github.com/smacker/go-tree-sitter" ) +func randomString(length int) string { + // generate random unicode string + var str string + for i := 0; i < length; i++ { + str += string(rune(rand.Intn(0x7E-0x21) + 0x21)) + } + return str +} + func isInside(ntype string, node *sitter.Node) bool { for node != nil { if node.Type() == ntype { @@ -23,7 +33,6 @@ func isInside(ntype string, node *sitter.Node) bool { func formatCode(sourceCode []byte, tree *sitter.Tree) string { node := tree.RootNode() - if node.HasError() { fmt.Println("Error parsing code") return string(sourceCode) @@ -68,7 +77,6 @@ func formatCode(sourceCode []byte, tree *sitter.Tree) string { writeIndent() formatted.WriteString("local ") case "name": - if parent.Parent().Type() == "call_stmt" && !isInside("local_var_stmt", &node) { if len(insideLocal) > 1 && insideLocal[len(insideLocal)-2] { formatted.WriteString("\n") @@ -232,9 +240,127 @@ func formatCode(sourceCode []byte, tree *sitter.Tree) string { return formatted.String() } +func compatibility(sourceCode []byte, tree *sitter.Tree) string { + node := tree.RootNode() + if node.HasError() { + fmt.Println("Error parsing code") + return string(sourceCode) + } + + // watch out for passing by reference + newSource := make([]byte, len(sourceCode)) + copy(newSource, sourceCode) + + type sub struct { + node *sitter.Node + toReplace string + } + var toSub []sub + + var findExprs func(node sitter.Node) + findExprs = func(node sitter.Node) { + start := node.StartByte() + end := node.EndByte() + rand := randomString(int(end - start)) + ntype := node.Type() + + if ntype == "ifexp" || (ntype == "binexp" && node.Child(1).Type() == "//") || (ntype == "var_stmt" && node.Child(1).Type() == "//=") { + // replace unsupported statements/expressions with a random string + newSource = append(newSource[:start], append([]byte(rand), newSource[end:]...)...) + toSub = append(toSub, sub{&node, rand}) + } else { + for i := range int(node.ChildCount()) { + findExprs(*(node.Child(i))) + } + } + } + + findExprs(*node) + sourceString := string(newSource) + + for _, s := range toSub { + var replacement string + + node := s.node + ntype := node.Type() + + switch ntype { + case "ifexp": + fmt.Println(c.InBlue("replacing")) + secondCond := node.Child(3).Type() + hasElseIf := node.Child(4).Type() == "elseif" + + if !hasElseIf && map[string]bool{ + "number": true, + "string": true, + "string_interp": true, + "true": true, // lelel + }[secondCond] { + // SPECIAL CASE: the second condition is guaranteed to be truthy + // this means it can be simplified to Lua's sorta-ternary operator, a and b or c + + // doesn't simplify nested if expressions. GOOD ENOUGH + for i := range int(node.ChildCount()) { + child := node.Child(i) + fmt.Println(child.Type()) + switch child.Type() { + case "then": + replacement += "and " + case "else": + replacement += "or " + case "elseif": + panic("elseif in special case") + case "if": + // nothing + default: + replacement += child.Content(sourceCode) + " " + } + } + } else { + replacement += "(function()" + for i := range int(node.ChildCount()) { + child := node.Child(i) + fmt.Println(child.Type()) + switch child.Type() { + case "if", "elseif", "then": + replacement += child.Content(sourceCode) + " " + case "else": + replacement += "end " + default: + prev := node.Child(i - 1).Type() + if prev == "then" || prev == "else" { + replacement += "return " + } + replacement += child.Content(sourceCode) + " " + } + } + replacement += "end)()" + } + case "binexp": + // child 1 is the operator (//) + left := node.Child(0).Content(sourceCode) + right := node.Child(2).Content(sourceCode) + replacement = fmt.Sprintf("math.floor(%s/%s)", left, right) + case "var_stmt": + // child 1 is the operator (//=) + left := node.Child(0).Content(sourceCode) + right := node.Child(2).Content(sourceCode) + + // todo: make it so if one of the exprs is a function it don't get evaluated twice + // nah jk im not doing that + replacement = fmt.Sprintf("%s=math.floor(%s/%s)", left, left, right) + default: + panic("unhandled node type " + ntype) + } + + sourceString = strings.Replace(sourceString, s.toReplace, replacement, 1) + } + + return sourceString +} + func main() { - parser := sitter.NewParser() - parser.SetLanguage(luau.GetLuau()) + binding := luau.GetLuau() filename := "test.luau" @@ -244,17 +370,27 @@ func main() { return } - tree, _ := parser.ParseCtx(context.Background(), nil, sourceCode) - formatted := formatCode(sourceCode, tree) + code := string(sourceCode) - // replace all ending newlines with a single newline - formatted = strings.Trim(formatted, "\n") + "\n" + // keep parsing until no changes are made lmao + for { + parser := sitter.NewParser() + parser.SetLanguage(binding) - // write back to file - err = os.WriteFile(filename, []byte(formatted), 0o644) - if err != nil { - fmt.Println(err) - return + newSource := []byte(code) + tree, err := parser.ParseCtx(context.Background(), nil, newSource) + if err != nil { + fmt.Println(err) + return + } + compatible := compatibility(newSource, tree) + // replace all ending newlines with a single newline + compatible = strings.Trim(compatible, "\n") + + if compatible == string(code) { + break + } + code = compatible } - + fmt.Println(code) }