diff --git a/src/cli.bux b/src/cli.bux index 6ada623..728d3c4 100644 --- a/src/cli.bux +++ b/src/cli.bux @@ -609,6 +609,44 @@ func Cli_Init() -> int { // Test command — build and run tests // --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Fmt command — format source files +// --------------------------------------------------------------------------- + +func Cli_Fmt(dir: String) -> int { + // If dir is a file, format just that file + if FileExists(dir) { + Print("Formatting "); + PrintLine(dir); + if Fmt_FormatFile(dir) { + PrintLine(" OK"); + return 0; + } + return 1; + } + // Otherwise format all .bux files in directory + Print("Formatting "); + PrintLine(dir); + var fileCount: int = 0; + let files: *String = bux_list_dir(dir, ".bux", &fileCount); + if fileCount == 0 { + PrintLine("No .bux files found"); + return 1; + } + var i: int = 0; + var okCount: int = 0; + while i < fileCount { + Print(" "); + PrintLine(files[i]); + if Fmt_FormatFile(files[i]) { + okCount = okCount + 1; + } + i = i + 1; + } + Print("Formatted "); PrintInt(okCount as int64); Print("/"); PrintInt(fileCount as int64); PrintLine(" files"); + return 0; +} + func Cli_Test(projectDir: String) -> int { Print("Testing project: "); PrintLine(projectDir); @@ -906,7 +944,7 @@ func Cli_Run(args: *String, argCount: int) -> int { if argCount < 2 { PrintLine("Bux Self-Hosting Compiler v0.2.0"); PrintLine("Usage: buxc [args]"); - PrintLine("Commands: build, check, new, init, test, run, project, help, version"); + PrintLine("Commands: build, check, new, init, fmt, test, run, project, help, version"); return 0; } @@ -919,7 +957,7 @@ func Cli_Run(args: *String, argCount: int) -> int { if String_Eq(cmd, "help") || String_Eq(cmd, "--help") || String_Eq(cmd, "-h") { PrintLine("Bux Self-Hosting Compiler v0.2.0"); PrintLine("Usage: buxc [args]"); - PrintLine("Commands: build, check, new, init, test, run, project, help, version"); + PrintLine("Commands: build, check, new, init, fmt, test, run, project, help, version"); PrintLine("Pipeline modules:"); PrintLine(" Lexer ✅ 695 lines"); PrintLine(" Parser ✅ 1004 lines"); @@ -942,6 +980,12 @@ func Cli_Run(args: *String, argCount: int) -> int { return Cli_Init(); } + if String_Eq(cmd, "fmt") { + let dir: String = "."; + if argCount >= 3 { dir = args[2]; } + return Cli_Fmt(dir); + } + if String_Eq(cmd, "check") { if argCount < 3 { PrintLine("Usage: buxc check "); diff --git a/src/fmt.bux b/src/fmt.bux new file mode 100644 index 0000000..49b7ba6 --- /dev/null +++ b/src/fmt.bux @@ -0,0 +1,138 @@ +// fmt.bux — Bux source code formatter (indentation-based, preserves line structure) +module Fmt { + +extern func bux_read_file(path: String) -> String; +extern func bux_write_file(path: String, content: String) -> bool; +extern func bux_strlen(s: String) -> uint; + +// Count leading spaces on a line +func Fmt_CountLeadingSpaces(line: String) -> int { + var count: int = 0; + while count < 256 { + let c: int = line[count] as int; + if c == 0 { break; } + if c != 32 { break; } // space + count = count + 1; + } + return count; +} + +// Skip whitespace from start of line, return rest +func Fmt_TrimLeft(line: String) -> String { + var i: int = 0; + while i < 256 { + let c: int = line[i] as int; + if c == 0 { break; } + if c != 32 && c != 9 { break; } + i = i + 1; + } + if i == 0 { return line; } + // Extract substring from i + var len: int = 0; + while len < 256 { + let c: int = line[len] as int; + if c == 0 { break; } + len = len + 1; + } + if i >= len { return ""; } + return bux_str_slice(line, i as uint, (len - i) as uint); +} + +// Check if char at position is inside a string or comment (simplified) +func Fmt_IsInStringOrComment(line: String, pos: int) -> bool { + var inString: bool = false; + var inChar: bool = false; + var inComment: bool = false; + var i: int = 0; + while i < pos { + let c: int = line[i] as int; + let n: int = 0; + if i + 1 < 256 { n = line[i + 1] as int; } + if inComment { i = i + 1; continue; } + if c == 47 && n == 47 { inComment = true; i = i + 1; continue; } // // + if c == 34 && !inChar { inString = !inString; } + if c == 39 && !inString { inChar = !inChar; } + i = i + 1; + } + return inString || inChar || inComment; +} + +// Count brace depth change on a line, skipping strings/comments +func Fmt_CountBraceDelta(line: String) -> int { + var delta: int = 0; + var i: int = 0; + while i < 256 { + let c: int = line[i] as int; + if c == 0 { break; } + if !Fmt_IsInStringOrComment(line, i) { + if c == 123 { delta = delta + 1; } // { + if c == 125 { delta = delta - 1; } // } + } + i = i + 1; + } + return delta; +} + +func Fmt_Format(source: String) -> String { + let sb: StringBuilder = StringBuilder_NewCap(8192); + var indent: int = 0; + var i: uint = 0; + let lineCount: uint = bux_str_split_count(source, "\n"); + + while i < lineCount { + let line: String = bux_str_split_part(source, "\n", i); + let trimmed: String = Fmt_TrimLeft(line); + + // Skip empty lines + if String_Eq(trimmed, "") { + StringBuilder_Append(&sb, "\n"); + i = i + 1; + continue; + } + + // Adjust indent for closing braces on this line + let delta: int = Fmt_CountBraceDelta(trimmed); + // If line starts with }, decrease indent before emitting + let firstChar: int = trimmed[0] as int; + if firstChar == 125 { // } + indent = indent - 1; + if indent < 0 { indent = 0; } + } + + // Emit indentation + var si: int = 0; + while si < indent { + StringBuilder_Append(&sb, " "); + si = si + 1; + } + + // Emit the trimmed line + StringBuilder_Append(&sb, trimmed); + StringBuilder_Append(&sb, "\n"); + + // Adjust indent for opening braces + if firstChar != 125 { + indent = indent + delta; + } else { + // For lines starting with }, apply the net delta after the initial decrease + indent = indent + delta + 1; + if indent < 0 { indent = 0; } + } + + i = i + 1; + } + + let result: String = StringBuilder_Build(&sb); + StringBuilder_Free(&sb); + return result; +} + +func Fmt_FormatFile(path: String) -> bool { + let source: String = bux_read_file(path); + if source == null as String || String_Eq(source, "") { return false; } + let formatted: String = Fmt_Format(source); + if String_Eq(formatted, "") { return false; } + return bux_write_file(path, formatted); +} + +}