Procházet zdrojové kódy

Merge pull request #997 from DanielGavin/prototype-fmt

Add odin/format and odin/printer packages
gingerBill před 4 roky
rodič
revize
362f07d7c5

+ 35 - 0
core/odin/format/format.odin

@@ -0,0 +1,35 @@
+package odin_format
+
+import "core:odin/printer"
+import "core:odin/parser"
+import "core:odin/ast"
+
+default_style := printer.default_style;
+
+simplify :: proc(file: ^ast.File) {
+
+}
+
+format :: proc(source: [] u8, config: printer.Config, allocator := context.allocator) -> ([] u8, bool) {
+
+    pkg := ast.Package {
+        kind = .Normal,
+    };
+
+    file := ast.File {
+        pkg = &pkg,
+        src = source,
+    };
+
+    p := parser.default_parser();
+
+    ok := parser.parse_file(&p, &file);
+
+    if !ok || file.syntax_error_count > 0  {
+        return {}, false;
+    }
+
+    prnt := printer.make_printer(config, allocator);
+
+    return transmute([]u8) printer.print(&prnt, &file), true;
+}

+ 2 - 0
core/odin/parser/parser.odin

@@ -2279,6 +2279,7 @@ parse_operand :: proc(p: ^Parser, lhs: bool) -> ^ast.Expr {
 			p.expr_level = -1;
 			where_clauses = parse_rhs_expr_list(p);
 			p.expr_level = prev_level;
+			tags = parse_proc_tags(p);
 		}
 		if p.allow_type && p.expr_level < 0 {
 			if where_token.kind != .Invalid {
@@ -3212,6 +3213,7 @@ parse_simple_stmt :: proc(p: ^Parser, flags: Stmt_Allow_Flags) -> ^ast.Stmt {
 					case ast.For_Stmt:         n.label = label;
 					case ast.Switch_Stmt:      n.label = label;
 					case ast.Type_Switch_Stmt: n.label = label;
+					case ast.Range_Stmt:	   n.label = label;
 					}
 				}
 

+ 921 - 0
core/odin/printer/printer.odin

@@ -0,0 +1,921 @@
+package odin_printer
+
+import "core:odin/ast"
+import "core:odin/tokenizer"
+import "core:strings"
+import "core:runtime"
+import "core:fmt"
+import "core:unicode/utf8"
+import "core:mem"
+
+Type_Enum :: enum {Line_Comment, Value_Decl, Switch_Stmt, Struct, Assign, Call, Enum, If, For, Proc_Lit}
+
+Line_Type :: bit_set[Type_Enum];
+
+/*
+	Represents an unwrapped line
+*/
+Line :: struct {
+	format_tokens: [dynamic]Format_Token,
+	finalized:     bool,
+	used:          bool,
+	depth:         int,
+	types:         Line_Type, //for performance, so you don't have to verify what types are in it by going through the tokens - might give problems when adding linebreaking
+}
+
+/*
+	Represents a singular token in a unwrapped line
+*/
+Format_Token :: struct {
+	kind:            tokenizer.Token_Kind,
+	text:            string,
+	type:            Type_Enum,
+	spaces_before:   int,
+	parameter_count: int,
+}
+
+Printer :: struct {
+	string_builder:       strings.Builder,
+	config:               Config,
+	depth:                int, //the identation depth
+	comments:             [dynamic]^ast.Comment_Group,
+	latest_comment_index: int,
+	allocator:            mem.Allocator,
+	file:                 ^ast.File,
+	source_position:      tokenizer.Pos,
+	last_source_position: tokenizer.Pos,
+	lines:                [dynamic]Line, //need to look into a better data structure, one that can handle inserting lines rather than appending
+	skip_semicolon:       bool,
+	current_line:         ^Line,
+	current_line_index:   int,
+	last_line_index:      int,
+	last_token:           ^Format_Token,
+	merge_next_token:     bool,
+	space_next_token:     bool,
+	debug:                bool,
+}
+
+Config :: struct {
+	spaces:               int,  //Spaces per indentation
+	newline_limit:        int,  //The limit of newlines between statements and declarations.
+	tabs:                 bool, //Enable or disable tabs
+	convert_do:           bool, //Convert all do statements to brace blocks
+	semicolons:           bool, //Enable semicolons
+	split_multiple_stmts: bool,
+	align_switch:         bool,
+	brace_style:          Brace_Style,
+	align_assignments:    bool,
+	align_structs:        bool,
+	align_style:          Alignment_Style,
+	align_enums:          bool,
+	align_length_break:   int,
+	indent_cases:         bool,
+	newline_style:        Newline_Style,
+}
+
+Brace_Style :: enum {
+	_1TBS,
+	Allman,
+	Stroustrup,
+	K_And_R,
+}
+
+Block_Type :: enum {
+	None,
+	If_Stmt,
+	Proc,
+	Generic,
+	Comp_Lit,
+	Switch_Stmt,
+}
+
+Alignment_Style :: enum {
+	Align_On_Colon_And_Equals,
+	Align_On_Type_And_Equals,
+}
+
+Newline_Style :: enum {
+	CRLF,
+	LF,
+}
+
+default_style := Config {
+	spaces = 4,
+	newline_limit = 2,
+	convert_do = false,
+	semicolons = true,
+	tabs = true,
+	brace_style = ._1TBS,
+	split_multiple_stmts = true,
+	align_assignments = true,
+	align_style = .Align_On_Type_And_Equals,
+	indent_cases = false,
+	align_switch = true,
+	align_structs = true,
+	align_enums = true,
+	newline_style = .CRLF,
+	align_length_break = 9,
+};
+
+make_printer :: proc(config: Config, allocator := context.allocator) -> Printer {
+	return {
+		config = config,
+		allocator = allocator,
+		debug = false,
+	};
+}
+
+print :: proc(p: ^Printer, file: ^ast.File) -> string {
+
+	p.comments = file.comments;
+
+	if len(file.decls) > 0 {
+		p.lines = make([dynamic]Line, 0, (file.decls[len(file.decls) - 1].end.line - file.decls[0].pos.line) * 2, context.temp_allocator);
+	}
+
+	set_source_position(p, file.pkg_token.pos);
+
+	p.last_source_position.line = 1;
+
+	set_line(p, 0);
+
+	push_generic_token(p, .Package, 0);
+	push_ident_token(p, file.pkg_name, 1);
+
+	for decl in file.decls {
+		visit_decl(p, cast(^ast.Decl)decl);
+	}
+
+	if len(p.comments) > 0 {
+		infinite := p.comments[len(p.comments) - 1].end;
+		infinite.offset = 9999999;
+		push_comments(p, infinite);
+	}
+
+	fix_lines(p);
+
+	builder := strings.make_builder(0, mem.megabytes(5), p.allocator);
+
+	last_line := 0;
+
+	newline: string;
+
+	if p.config.newline_style == .LF {
+		newline = "\n";
+	} else {
+		newline = "\r\n";
+	}
+
+	for line, line_index in p.lines {
+		diff_line := line_index - last_line;
+
+		for i := 0; i < diff_line; i += 1 {
+			strings.write_string(&builder, newline);
+		}
+
+		if p.config.tabs {
+			for i := 0; i < line.depth; i += 1 {
+				strings.write_byte(&builder, '\t');
+			}
+		} else {
+			for i := 0; i < line.depth * p.config.spaces; i += 1 {
+				strings.write_byte(&builder, ' ');
+			}
+		}
+
+		if p.debug {
+			strings.write_string(&builder, fmt.tprintf("line %v: ", line_index));
+		}
+
+		for format_token in line.format_tokens {
+
+			for i := 0; i < format_token.spaces_before; i += 1 {
+				strings.write_byte(&builder, ' ');
+			}
+
+			strings.write_string(&builder, format_token.text);
+		}
+
+		last_line = line_index;
+	}
+
+	strings.write_string(&builder, newline);
+
+	return strings.to_string(builder);
+}
+
+fix_lines :: proc(p: ^Printer) {
+	align_var_decls(p);
+	format_generic(p);
+	align_comments(p); //align them last since they rely on the other alignments
+}
+
+format_value_decl :: proc(p: ^Printer, index: int) {
+
+	eq_found := false;
+	eq_token: Format_Token;
+	eq_line:  int;
+	largest := 0;
+
+	found_eq: for line, line_index in p.lines[index:] {
+		for format_token in line.format_tokens {
+
+			largest += len(format_token.text) + format_token.spaces_before;
+
+			if format_token.kind == .Eq {
+				eq_token = format_token;
+				eq_line = line_index + index;
+				eq_found = true;
+				break found_eq;
+			}
+		}
+	}
+
+	if !eq_found {
+		return;
+	}
+
+	align_next := false;
+
+	//check to see if there is a binary operator in the last token(this is guaranteed by the ast visit), otherwise it's not multilined
+	for line, line_index in p.lines[eq_line:] {
+
+		if len(line.format_tokens) == 0 {
+			break;
+		}
+
+		if align_next {
+			line.format_tokens[0].spaces_before = largest + 1;
+			align_next = false;
+		}
+
+		kind := find_last_token(line.format_tokens).kind;
+
+		if tokenizer.Token_Kind.B_Operator_Begin < kind && kind <= tokenizer.Token_Kind.Cmp_Or {
+			align_next = true;
+		}
+
+		if !align_next {
+			break;
+		}
+	}
+}
+
+find_last_token :: proc(format_tokens: [dynamic]Format_Token) -> Format_Token {
+
+	for i := len(format_tokens) - 1; i >= 0; i -= 1 {
+
+		if format_tokens[i].kind != .Comment {
+			return format_tokens[i];
+		}
+	}
+
+	panic("not possible");
+}
+
+format_assignment :: proc(p: ^Printer, index: int) {
+}
+
+format_call :: proc(p: ^Printer, line_index: int, format_index: int) {
+
+	paren_found := false;
+	paren_token:       Format_Token;
+	paren_line:        int;
+	paren_token_index: int;
+	largest := 0;
+
+	found_paren: for line, i in p.lines[line_index:] {
+		for format_token, j in line.format_tokens {
+
+			largest += len(format_token.text) + format_token.spaces_before;
+
+			if i == 0 && j < format_index {
+				continue;
+			}
+
+			if format_token.kind == .Open_Paren && format_token.type == .Call {
+				paren_token = format_token;
+				paren_line = line_index + i;
+				paren_found = true;
+				paren_token_index = j;
+				break found_paren;
+			}
+		}
+	}
+
+	if !paren_found {
+		panic("Should not be possible");
+	}
+
+	paren_count := 1;
+	done        := false;
+
+	for line, line_index in p.lines[paren_line:] {
+
+		if len(line.format_tokens) == 0 {
+			continue;
+		}
+
+		for format_token, i in line.format_tokens {
+
+			if format_token.kind == .Comment {
+				continue;
+			}
+
+			if line_index == 0 && i <= paren_token_index {
+				continue;
+			}
+
+			if format_token.kind == .Open_Paren {
+				paren_count += 1;
+			} else if format_token.kind == .Close_Paren {
+				paren_count -= 1;
+			}
+
+			if paren_count == 0 {
+				done = true;
+			}
+		}
+
+		if line_index != 0 {
+			line.format_tokens[0].spaces_before = largest;
+		}
+
+		if done {
+			return;
+		}
+	}
+}
+
+format_keyword_to_brace :: proc(p: ^Printer, line_index: int, format_index: int, keyword: tokenizer.Token_Kind) {
+
+	keyword_found := false;
+	keyword_token: Format_Token;
+	keyword_line:  int;
+
+	largest := 0;
+	brace_count := 0;
+	done        := false;
+
+	found_keyword: for line, i in p.lines[line_index:] {
+		for format_token in line.format_tokens {
+
+			largest += len(format_token.text) + format_token.spaces_before;
+
+			if format_token.kind == keyword {
+				keyword_token = format_token;
+				keyword_line = line_index + i;
+				keyword_found = true;
+				break found_keyword;
+			}
+		}
+	}
+
+	if !keyword_found {
+		panic("Should not be possible");
+	}
+
+	for line, line_index in p.lines[keyword_line:] {
+
+		if len(line.format_tokens) == 0 {
+			continue;
+		}
+
+		for format_token, i in line.format_tokens {
+
+			if format_token.kind == .Comment {
+				break;
+			} else if format_token.kind == .Undef {
+				return;
+			} 
+
+			if line_index == 0 && i <= format_index {
+				continue;
+			}
+
+			if format_token.kind == .Open_Brace {
+				brace_count += 1;
+			} else if format_token.kind == .Close_Brace {
+				brace_count -= 1;
+			}
+
+			if brace_count == 1 {
+				done = true;
+			}
+		}
+
+		if line_index != 0 {
+			line.format_tokens[0].spaces_before = largest + 1;
+		}
+
+		if done {
+			return;
+		}
+	}
+}
+
+format_generic :: proc(p: ^Printer) {
+
+	next_struct_line := 0;
+
+	for line, line_index in p.lines {
+
+		if len(line.format_tokens) <= 0 {
+			continue;
+		}
+
+		for format_token, token_index in line.format_tokens {
+			if format_token.kind == .For || format_token.kind == .If ||
+			   format_token.kind == .When || format_token.kind == .Switch ||
+			   (format_token.kind == .Proc && format_token.type == .Proc_Lit) {
+				format_keyword_to_brace(p, line_index, token_index, format_token.kind);
+			} else if format_token.type == .Call {
+				format_call(p, line_index, token_index);
+			}
+		}
+
+		if .Switch_Stmt in line.types && p.config.align_switch {
+			align_switch_stmt(p, line_index);
+		}
+
+		if .Enum in line.types && p.config.align_enums {
+			align_enum(p, line_index);
+		}
+
+		if .Struct in line.types && p.config.align_structs && next_struct_line <= 0 {
+			next_struct_line = align_struct(p, line_index);
+		}
+
+		if .Value_Decl in line.types {
+			format_value_decl(p, line_index);
+		}
+
+		if .Assign in line.types {
+			format_assignment(p, line_index);
+		}
+
+		next_struct_line -= 1;
+	}
+}
+
+align_var_decls :: proc(p: ^Printer) {
+
+	current_line:        int;
+	current_typed:       bool;
+	current_not_mutable: bool;
+
+	largest_lhs := 0;
+	largest_rhs := 0;
+
+	TokenAndLength :: struct {
+		format_token: ^Format_Token,
+		length:       int,
+	};
+
+	colon_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator);
+	type_tokens  := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator);
+	equal_tokens := make([dynamic]TokenAndLength, 0, 10, context.temp_allocator);
+
+	for line, line_index in p.lines {
+
+		//It is only possible to align value decls that are one one line, otherwise just ignore them
+		if .Value_Decl not_in line.types {
+			continue;
+		}
+
+		typed         := true;
+		not_mutable   := false;
+		continue_flag := false;
+
+		for i := 0; i < len(line.format_tokens); i += 1 {
+			if line.format_tokens[i].kind == .Colon && line.format_tokens[min(i + 1, len(line.format_tokens) - 1)].kind == .Eq {
+				typed = false;
+			}
+
+			if line.format_tokens[i].kind == .Colon && line.format_tokens[min(i + 1, len(line.format_tokens) - 1)].kind == .Colon {
+				not_mutable = true;
+			}
+
+			if line.format_tokens[i].kind == .Union ||
+			   line.format_tokens[i].kind == .Enum ||
+			   line.format_tokens[i].kind == .Struct ||
+			   line.format_tokens[i].kind == .For ||
+			   line.format_tokens[i].kind == .If || 
+			   line.format_tokens[i].kind == .Comment {
+				continue_flag = true;
+			}
+
+			//enforced undef is always on the last line, if it exists
+			if line.format_tokens[i].kind == .Proc && line.format_tokens[len(line.format_tokens)-1].kind != .Undef {
+				continue_flag = true;
+			}
+
+		}
+
+		if continue_flag {
+			continue;
+		}
+
+		if line_index != current_line + 1 || typed != current_typed || not_mutable != current_not_mutable {
+
+			if p.config.align_style == .Align_On_Colon_And_Equals || !current_typed || current_not_mutable {
+				for colon_token in colon_tokens {
+					colon_token.format_token.spaces_before = largest_lhs - colon_token.length + 1;
+				}
+			} else if p.config.align_style == .Align_On_Type_And_Equals {
+				for type_token in type_tokens {
+					type_token.format_token.spaces_before = largest_lhs - type_token.length + 1;
+				}
+			}
+
+			if current_typed {
+				for equal_token in equal_tokens {
+					equal_token.format_token.spaces_before = largest_rhs - equal_token.length + 1;
+				}
+			} else {
+				for equal_token in equal_tokens {
+					equal_token.format_token.spaces_before = 0;
+				}
+			}
+
+			clear(&colon_tokens);
+			clear(&type_tokens);
+			clear(&equal_tokens);
+
+			largest_rhs = 0;
+			largest_lhs = 0;
+			current_typed = typed;
+			current_not_mutable = not_mutable;
+		}
+
+		current_line = line_index;
+
+		current_token_index := 0;
+		lhs_length          := 0;
+		rhs_length          := 0;
+
+		//calcuate the length of lhs of a value decl i.e. `a, b:`
+		for; current_token_index < len(line.format_tokens); current_token_index += 1 {
+
+			lhs_length += len(line.format_tokens[current_token_index].text) + line.format_tokens[current_token_index].spaces_before;
+
+			if line.format_tokens[current_token_index].kind == .Colon {
+				append(&colon_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index], length = lhs_length});
+
+				if len(line.format_tokens) > current_token_index && line.format_tokens[current_token_index + 1].kind != .Eq {
+					append(&type_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index + 1], length = lhs_length});
+				}
+
+				current_token_index += 1;
+				largest_lhs = max(largest_lhs, lhs_length);
+				break;
+			}
+		}
+
+		//calcuate the length of the rhs i.e. `[dynamic]int = 123123`
+		for; current_token_index < len(line.format_tokens); current_token_index += 1 {
+
+			rhs_length += len(line.format_tokens[current_token_index].text) + line.format_tokens[current_token_index].spaces_before;
+
+			if line.format_tokens[current_token_index].kind == .Eq {
+				append(&equal_tokens, TokenAndLength {format_token = &line.format_tokens[current_token_index], length = rhs_length});
+				largest_rhs = max(largest_rhs, rhs_length);
+				break;
+			}
+		}
+		
+	}
+
+	//repeating myself, move to sub procedure
+	if p.config.align_style == .Align_On_Colon_And_Equals || !current_typed || current_not_mutable {
+		for colon_token in colon_tokens {
+			colon_token.format_token.spaces_before = largest_lhs - colon_token.length + 1;
+		}
+	} else if p.config.align_style == .Align_On_Type_And_Equals {
+		for type_token in type_tokens {
+			type_token.format_token.spaces_before = largest_lhs - type_token.length + 1;
+		}
+	}
+
+	if current_typed {
+		for equal_token in equal_tokens {
+			equal_token.format_token.spaces_before = largest_rhs - equal_token.length + 1;
+		}
+	} else {
+		for equal_token in equal_tokens {
+			equal_token.format_token.spaces_before = 0;
+		}
+	}
+}
+
+align_switch_stmt :: proc(p: ^Printer, index: int) {
+	switch_found := false;
+	brace_token: Format_Token;
+	brace_line:  int;
+
+	found_switch_brace: for line, line_index in p.lines[index:] {
+		for format_token in line.format_tokens {
+			if format_token.kind == .Open_Brace && switch_found {
+				brace_token = format_token;
+				brace_line = line_index + index;
+				break found_switch_brace;
+			} else if format_token.kind == .Open_Brace {
+				break;
+			} else if format_token.kind == .Switch {
+				switch_found = true;
+			}
+		}
+	}
+
+	if !switch_found {
+		return;
+	}
+
+	largest    := 0;
+	case_count := 0;
+
+	TokenAndLength :: struct {
+		format_token: ^Format_Token,
+		length:       int,
+	};
+
+	format_tokens := make([dynamic]TokenAndLength, 0, brace_token.parameter_count, context.temp_allocator);
+
+	//find all the switch cases that are one lined
+	for line, line_index in p.lines[brace_line + 1:] {
+
+		case_found  := false;
+		colon_found := false;
+		length      := 0;
+
+		for format_token, i in line.format_tokens {
+
+			if format_token.kind == .Comment {
+				break;
+			}
+
+			//this will only happen if the case is one lined
+			if case_found && colon_found {
+				append(&format_tokens, TokenAndLength {format_token = &line.format_tokens[i], length = length});
+				largest = max(length, largest);
+				break;
+			}
+
+			if format_token.kind == .Case {
+				case_found = true;
+				case_count += 1;
+			} else if format_token.kind == .Colon {
+				colon_found = true;
+			}
+
+			length += len(format_token.text) + format_token.spaces_before;
+		}
+
+		if case_count >= brace_token.parameter_count {
+			break;
+		}
+	}
+
+	for token in format_tokens {
+		token.format_token.spaces_before = largest - token.length + 1;
+	}
+
+}
+
+align_enum :: proc(p: ^Printer, index: int) {
+	enum_found := false;
+	brace_token: Format_Token;
+	brace_line:  int;
+
+	found_enum_brace: for line, line_index in p.lines[index:] {
+		for format_token in line.format_tokens {
+			if format_token.kind == .Open_Brace && enum_found {
+				brace_token = format_token;
+				brace_line = line_index + index;
+				break found_enum_brace;
+			} else if format_token.kind == .Open_Brace {
+				break;
+			} else if format_token.kind == .Enum {
+				enum_found = true;
+			}
+		}
+	}
+
+	if !enum_found {
+		return;
+	}
+
+	largest     := 0;
+	comma_count := 0;
+
+	TokenAndLength :: struct {
+		format_token: ^Format_Token,
+		length:       int,
+	};
+
+	format_tokens := make([dynamic]TokenAndLength, 0, brace_token.parameter_count, context.temp_allocator);
+
+	for line, line_index in p.lines[brace_line + 1:] {
+		length := 0;
+
+		for format_token, i in line.format_tokens {
+			if format_token.kind == .Comment {
+				break;
+			}
+
+			if format_token.kind == .Eq {
+				append(&format_tokens, TokenAndLength {format_token = &line.format_tokens[i], length = length});
+				largest = max(length, largest);
+				break;
+			} else if format_token.kind == .Comma {
+				comma_count += 1;
+			}
+
+			length += len(format_token.text) + format_token.spaces_before;
+		}
+
+		if comma_count >= brace_token.parameter_count {
+			break;
+		}
+	}
+
+	for token in format_tokens {
+		token.format_token.spaces_before = largest - token.length + 1;
+	}
+
+}
+
+align_struct :: proc(p: ^Printer, index: int) -> int {
+	struct_found := false;
+	brace_token: Format_Token;
+	brace_line:  int;
+
+	found_struct_brace: for line, line_index in p.lines[index:] {
+		for format_token in line.format_tokens {
+			if format_token.kind == .Open_Brace && struct_found {
+				brace_token = format_token;
+				brace_line = line_index + index;
+				break found_struct_brace;
+			} else if format_token.kind == .Open_Brace {
+				break;
+			} else if format_token.kind == .Struct {
+				struct_found = true;
+			}
+		}
+	}
+
+	if !struct_found {
+		return 0;
+	}
+
+	largest     := 0;
+	colon_count := 0;
+	nested      := false;
+	seen_brace  := false;
+
+	TokenAndLength :: struct {
+		format_token: ^Format_Token,
+		length:       int,
+	};
+
+	format_tokens := make([]TokenAndLength, brace_token.parameter_count, context.temp_allocator);
+
+	if brace_token.parameter_count == 0 {
+		return 0;
+	}
+
+	end_line_index := 0;
+
+	for line, line_index in p.lines[brace_line + 1:] {
+		length := 0;
+
+		for format_token, i in line.format_tokens {
+			
+			//give up on nested structs
+			if format_token.kind == .Comment {
+				break;
+			} else if format_token.kind == .Open_Paren {
+				break;
+			} else if format_token.kind == .Open_Brace {
+				seen_brace = true;
+			} else if format_token.kind == .Close_Brace {
+				seen_brace = false;
+			} else if seen_brace {
+				continue;
+			}
+
+			if format_token.kind == .Colon {
+				format_tokens[colon_count] = {format_token = &line.format_tokens[i + 1], length = length};
+
+				if format_tokens[colon_count].format_token.kind == .Struct {
+					nested = true;
+				}
+
+				colon_count += 1;
+				largest = max(length, largest);
+			}
+
+			length += len(format_token.text) + format_token.spaces_before;
+		}
+
+		if nested {
+			end_line_index = line_index + brace_line + 1;
+		}
+
+		if colon_count >= brace_token.parameter_count {
+			break;
+		}
+	} 
+
+	//give up aligning nested, it never looks good
+	if nested {
+		for line, line_index in p.lines[end_line_index:] {
+			for format_token in line.format_tokens {
+				if format_token.kind == .Close_Brace {
+					return end_line_index + line_index - index;
+				} 
+			}
+		}
+	}
+
+	for token in format_tokens {
+		token.format_token.spaces_before = largest - token.length + 1;
+	}
+
+	return 0;
+}
+
+align_comments :: proc(p: ^Printer) {
+
+	Comment_Align_Info :: struct {
+		length: int,
+		begin:  int,
+		end:    int,
+		depth:  int,
+	};
+
+	comment_infos := make([dynamic]Comment_Align_Info, 0, context.temp_allocator);
+
+	current_info: Comment_Align_Info;
+
+	for line, line_index in p.lines {
+		if len(line.format_tokens) <= 0 {
+			continue;
+		}
+
+		if .Line_Comment in line.types {
+			if current_info.end + 1 != line_index || current_info.depth != line.depth ||
+			   (current_info.begin == current_info.end && current_info.length == 0) {
+
+				if (current_info.begin != 0 && current_info.end != 0) || current_info.length > 0 {
+					append(&comment_infos, current_info);
+				}
+
+				current_info.begin = line_index;
+				current_info.end = line_index;
+				current_info.depth = line.depth;
+				current_info.length = 0;
+			}
+
+			length := 0;
+
+			for format_token, i in line.format_tokens {
+				if format_token.kind == .Comment {
+					current_info.length = max(current_info.length, length);
+					current_info.end = line_index;
+				}
+
+				length += format_token.spaces_before + len(format_token.text);
+			}
+		}
+	}
+
+	if (current_info.begin != 0 && current_info.end != 0) || current_info.length > 0 {
+		append(&comment_infos, current_info);
+	}
+
+	for info in comment_infos {
+
+		if info.begin == info.end || info.length == 0 {
+			continue;
+		}
+
+		for i := info.begin; i <= info.end; i += 1 {
+			l := p.lines[i];
+
+			length := 0;
+
+			for format_token, i in l.format_tokens {
+				if format_token.kind == .Comment {
+					if len(l.format_tokens) == 1 {
+						l.format_tokens[i].spaces_before = info.length + 1;
+					} else {
+						l.format_tokens[i].spaces_before = info.length - length + 1;
+					}
+				}
+
+				length += format_token.spaces_before + len(format_token.text);
+			}
+		}
+	}
+}

+ 1525 - 0
core/odin/printer/visit.odin

@@ -0,0 +1,1525 @@
+package odin_printer
+
+import "core:odin/ast"
+import "core:odin/tokenizer"
+import "core:strings"
+import "core:runtime"
+import "core:fmt"
+import "core:unicode/utf8"
+import "core:mem"
+import "core:sort"
+
+//right now the attribute order is not linearly parsed(bug?)
+@(private)
+sort_attribute :: proc(s: ^[dynamic]^ast.Attribute) -> sort.Interface {
+	return sort.Interface {
+		collection = rawptr(s),
+		len = proc(it: sort.Interface) -> int {
+			s := (^[dynamic]^ast.Attribute)(it.collection);
+			return len(s^);
+		},
+		less = proc(it: sort.Interface, i, j: int) -> bool {
+			s := (^[dynamic]^ast.Attribute)(it.collection);
+			return s[i].pos.offset < s[j].pos.offset;
+		},
+		swap = proc(it: sort.Interface, i, j: int) {
+			s := (^[dynamic]^ast.Attribute)(it.collection);
+			s[i], s[j] = s[j], s[i];
+		},
+	};
+}
+
+@(private)
+comment_before_position :: proc(p: ^Printer, pos: tokenizer.Pos) -> bool {
+
+	if len(p.comments) <= p.latest_comment_index {
+		return false;
+	}
+
+	comment := p.comments[p.latest_comment_index];
+
+	return comment.pos.offset < pos.offset;
+}
+
+@(private)
+next_comment_group :: proc(p: ^Printer) {
+	p.latest_comment_index += 1;
+}
+
+@(private)
+push_comment :: proc(p: ^Printer, comment: tokenizer.Token) -> int {
+
+	if len(comment.text) == 0 {
+		return 0;
+	}
+
+	if comment.text[0] == '/' && comment.text[1] == '/' {
+		format_token := Format_Token {
+			spaces_before = 1,
+			kind = .Comment,
+			text = comment.text,
+		};
+
+		if len(p.current_line.format_tokens) == 0 {
+			format_token.spaces_before = 0;
+		}
+
+		if !p.current_line.used {
+			p.current_line.used = true;
+			p.current_line.depth = p.depth;
+		}
+
+		append(&p.current_line.format_tokens, format_token);
+		p.last_token = &p.current_line.format_tokens[len(p.current_line.format_tokens) - 1];
+
+		hint_current_line(p, {.Line_Comment});
+
+		return 0;
+	} else {
+
+		builder := strings.make_builder(context.temp_allocator);
+
+		c_len      := len(comment.text);
+		trim_space := true;
+
+		multilines: [dynamic]string;
+
+		for i := 0; i < len(comment.text); i += 1 {
+
+			c := comment.text[i];
+
+			if c != ' ' && c != '\t' {
+				trim_space = false;
+			}
+
+			if (c == ' ' || c == '\t' || c == '\n') && trim_space {
+				continue;
+			} else if c == 13 && comment.text[min(c_len - 1, i + 1)] == 10 {
+				append(&multilines, strings.to_string(builder));
+				builder = strings.make_builder(context.temp_allocator);
+				trim_space = true;
+				i += 1;
+			} else if c == 10 {
+				append(&multilines, strings.to_string(builder));
+				builder = strings.make_builder(context.temp_allocator);
+				trim_space = true;
+			} else if c == '/' && comment.text[min(c_len - 1, i + 1)] == '*' {
+				strings.write_string(&builder, "/*");
+				trim_space = true;
+				i += 1;
+			} else if c == '*' && comment.text[min(c_len - 1, i + 1)] == '/' {
+				trim_space = true;
+				strings.write_string(&builder, "*/");
+				i += 1;
+			} else {
+				strings.write_byte(&builder, c);
+			}
+		}
+
+		if strings.builder_len(builder) > 0 {
+			append(&multilines, strings.to_string(builder));
+		}
+
+		for line in multilines {
+			format_token := Format_Token {
+				spaces_before = 1,
+				kind = .Comment,
+				text = line,
+			};
+
+			if len(p.current_line.format_tokens) == 0 {
+				format_token.spaces_before = 0;
+			}
+
+			if strings.contains(line, "*/") {
+				unindent(p);
+			}
+
+			if !p.current_line.used {
+				p.current_line.used = true;
+				p.current_line.depth = p.depth;
+			}
+
+			append(&p.current_line.format_tokens, format_token);
+			p.last_token = &p.current_line.format_tokens[len(p.current_line.format_tokens) - 1];
+
+			if strings.contains(line, "/*") {
+				indent(p);
+			}
+
+			newline_position(p, 1);
+		}
+
+		return len(multilines);
+	}
+}
+
+@(private)
+push_comments :: proc(p: ^Printer, pos: tokenizer.Pos) {
+
+	prev_comment:       ^tokenizer.Token;
+	prev_comment_lines: int;
+
+	for comment_before_position(p, pos) {
+
+		comment_group := p.comments[p.latest_comment_index];
+
+		if prev_comment == nil {
+			lines := comment_group.pos.line - p.last_source_position.line;
+			set_line(p, p.last_line_index + min(p.config.newline_limit, lines));
+		}
+
+		for comment, i in comment_group.list {
+
+			if prev_comment != nil && p.last_source_position.line != comment.pos.line {
+				newline_position(p, min(p.config.newline_limit, comment.pos.line - prev_comment.pos.line - prev_comment_lines));
+			}
+
+			prev_comment_lines = push_comment(p, comment);
+			prev_comment = &comment_group.list[i];
+		}
+
+		next_comment_group(p);
+	}
+
+	if prev_comment != nil {
+		newline_position(p, min(p.config.newline_limit, p.source_position.line - prev_comment.pos.line - prev_comment_lines));
+	}
+}
+
+@(private)
+append_format_token :: proc(p: ^Printer, format_token: Format_Token) -> ^Format_Token {
+
+	format_token := format_token;
+
+	if p.last_token != nil && (p.last_token.kind == .Ellipsis || p.last_token.kind == .Range_Half ||
+	   p.last_token.kind == .Open_Paren || p.last_token.kind == .Period ||
+	   p.last_token.kind == .Open_Brace || p.last_token.kind == .Open_Bracket) {
+		format_token.spaces_before = 0;
+	} else if p.merge_next_token {
+		format_token.spaces_before = 0;
+		p.merge_next_token = false;
+	} else if p.space_next_token {
+		format_token.spaces_before = 1;
+		p.space_next_token = false;
+	}
+
+	push_comments(p, p.source_position);
+
+	unwrapped_line := p.current_line;
+
+	if !unwrapped_line.used {
+		unwrapped_line.used = true;
+		unwrapped_line.depth = p.depth;
+	}
+
+	if len(unwrapped_line.format_tokens) == 0 && format_token.spaces_before == 1 {
+		format_token.spaces_before = 0;
+	}
+
+	p.last_source_position = p.source_position;
+	p.last_line_index = p.current_line_index;
+
+	append(&unwrapped_line.format_tokens, format_token);
+	return &unwrapped_line.format_tokens[len(unwrapped_line.format_tokens) - 1];
+}
+
+@(private)
+push_format_token :: proc(p: ^Printer, format_token: Format_Token) {
+	p.last_token = append_format_token(p, format_token);
+}
+
+@(private)
+push_generic_token :: proc(p: ^Printer, kind: tokenizer.Token_Kind, spaces_before: int, value := "") {
+
+	format_token := Format_Token {
+		spaces_before = spaces_before,
+		kind = kind,
+		text = tokenizer.tokens[kind],
+	};
+
+	if value != "" {
+		format_token.text = value;
+	}
+
+	p.last_token = append_format_token(p, format_token);
+}
+
+@(private)
+push_string_token :: proc(p: ^Printer, text: string, spaces_before: int) {
+
+	format_token := Format_Token {
+		spaces_before = spaces_before,
+		kind = .String,
+		text = text,
+	};
+
+	p.last_token = append_format_token(p, format_token);
+}
+
+@(private)
+push_ident_token :: proc(p: ^Printer, text: string, spaces_before: int) {
+
+	format_token := Format_Token {
+		spaces_before = spaces_before,
+		kind = .Ident,
+		text = text,
+	};
+
+	p.last_token = append_format_token(p, format_token);
+}
+
+@(private)
+set_source_position :: proc(p: ^Printer, pos: tokenizer.Pos) {
+	p.source_position = pos;
+}
+
+@(private)
+move_line :: proc(p: ^Printer, pos: tokenizer.Pos) {
+	move_line_limit(p, pos, p.config.newline_limit);
+}
+
+@(private)
+move_line_limit :: proc(p: ^Printer, pos: tokenizer.Pos, limit: int) -> bool {
+	lines := min(pos.line - p.source_position.line, limit);
+
+	if lines < 0 {
+		return false;
+	}
+
+	p.source_position = pos;
+	p.current_line_index += lines;
+	set_line(p, p.current_line_index);
+	return lines > 0;
+}
+
+@(private)
+set_line :: proc(p: ^Printer, line: int) -> ^Line {
+
+	unwrapped_line: ^Line;
+
+	if line >= len(p.lines) {
+		for i := len(p.lines); i <= line; i += 1 {
+			new_line: Line;
+			new_line.format_tokens = make([dynamic]Format_Token, 0, 50, p.allocator);
+			append(&p.lines, new_line);
+		}
+		unwrapped_line = &p.lines[line];
+	} else {
+		unwrapped_line = &p.lines[line];
+	}
+
+	p.current_line = unwrapped_line;
+	p.current_line_index = line;
+
+	return unwrapped_line;
+}
+
+@(private)
+newline_position :: proc(p: ^Printer, count: int) {
+	p.current_line_index += count;
+	set_line(p, p.current_line_index);
+}
+
+@(private)
+indent :: proc(p: ^Printer) {
+	p.depth += 1;
+}
+
+@(private)
+unindent :: proc(p: ^Printer) {
+	p.depth -= 1;
+}
+
+@(private)
+merge_next_token :: proc(p: ^Printer) {
+	p.merge_next_token = true;
+}
+
+@(private)
+space_next_token :: proc(p: ^Printer) {
+	p.space_next_token = true;
+}
+
+@(private)
+hint_current_line :: proc(p: ^Printer, hint: Line_Type) {
+	p.current_line.types |= hint;
+}
+
+@(private)
+visit_decl :: proc(p: ^Printer, decl: ^ast.Decl, called_in_stmt := false) {
+
+	using ast;
+
+	if decl == nil {
+		return;
+	}
+
+	switch v in &decl.derived {
+	case Expr_Stmt:
+		move_line(p, decl.pos);
+		visit_expr(p, v.expr);
+		if p.config.semicolons {
+			push_generic_token(p, .Semicolon, 0);
+		}
+	case When_Stmt:
+		visit_stmt(p, cast(^Stmt)decl);
+	case Foreign_Import_Decl:
+		if len(v.attributes) > 0 {
+			sort.sort(sort_attribute(&v.attributes));
+			move_line(p, v.attributes[0].pos);
+			visit_attributes(p, v.attributes);
+		}
+
+		move_line(p, decl.pos);
+
+		push_generic_token(p, v.foreign_tok.kind, 0);
+		push_generic_token(p, v.import_tok.kind, 1);
+
+		if v.name != nil {
+			push_ident_token(p, v.name.name, 1);
+		}
+
+		for path in v.fullpaths {
+			push_ident_token(p, path, 0);
+		}
+	case Foreign_Block_Decl:
+
+		if len(v.attributes) > 0 {
+			sort.sort(sort_attribute(&v.attributes));
+			move_line(p, v.attributes[0].pos);
+			visit_attributes(p, v.attributes);
+		}
+
+		move_line(p, decl.pos);
+
+		push_generic_token(p, .Foreign, 0);
+
+		visit_expr(p, v.foreign_library);
+		visit_stmt(p, v.body);
+	case Import_Decl:
+		move_line(p, decl.pos);
+
+		if v.name.text != "" {
+			push_generic_token(p, v.import_tok.kind, 1);
+			push_generic_token(p, v.name.kind, 1, v.name.text);
+			push_ident_token(p, v.fullpath, 1);
+		} else {
+			push_generic_token(p, v.import_tok.kind, 1);
+			push_ident_token(p, v.fullpath, 1);
+		}
+
+	case Value_Decl:
+		if len(v.attributes) > 0 {
+			sort.sort(sort_attribute(&v.attributes));
+			move_line(p, v.attributes[0].pos);
+			visit_attributes(p, v.attributes);
+		}
+
+		move_line(p, decl.pos);
+
+		if v.is_using {
+			push_generic_token(p, .Using, 0);
+		}
+
+		visit_exprs(p, v.names, true);
+
+		hint_current_line(p, {.Value_Decl});
+
+		if v.type != nil {
+			if !v.is_mutable {
+				push_generic_token(p, .Colon, 0);
+			} else {
+				push_generic_token(p, .Colon, 0);
+			}
+
+			visit_expr(p, v.type);
+		} else {
+			if !v.is_mutable {
+				push_generic_token(p, .Colon, 1);
+				push_generic_token(p, .Colon, 0);
+			} else {
+				push_generic_token(p, .Colon, 1);
+			}
+		}
+
+		if v.is_mutable && v.type != nil && len(v.values) != 0 {
+			push_generic_token(p, .Eq, 1);
+		} else if v.is_mutable && v.type == nil && len(v.values) != 0 {
+			push_generic_token(p, .Eq, 0);
+		} else if !v.is_mutable && v.type != nil {
+			push_generic_token(p, .Colon, 0);
+		}
+
+		if len(v.values) == 1 {
+			visit_expr(p, v.values[0]); //this is too ensure that one value are never newlined(procs, structs, etc.)
+		} else {
+			visit_exprs(p, v.values, true);
+		}
+
+		add_semicolon := true;
+
+		for value in v.values {
+			switch a in value.derived {
+			case Union_Type, Enum_Type, Struct_Type:
+				add_semicolon = false || called_in_stmt;
+			case Proc_Lit:
+				add_semicolon = false;
+			}
+		}
+
+		if add_semicolon && p.config.semicolons && !p.skip_semicolon {
+			push_generic_token(p, .Semicolon, 0);
+		}
+
+	case:
+		panic(fmt.aprint(decl.derived));
+	}
+}
+
+@(private)
+visit_exprs :: proc(p: ^Printer, list: []^ast.Expr, add_comma := false, trailing := false, force_newline := false) {
+
+	if len(list) == 0 {
+		return;
+	}
+
+	//we have to newline the expressions to respect the source
+	for expr, i in list {
+		//Don't move the first expression, it looks bad
+		if i != 0 && force_newline {
+			newline_position(p, 1);
+		} else if i != 0 {
+			move_line_limit(p, expr.pos, 1);
+		}
+
+		visit_expr(p, expr);
+
+		if (i != len(list) - 1 || trailing) && add_comma {
+			push_generic_token(p, .Comma, 0);
+		}
+	}
+
+	if len(list) > 1 && force_newline {
+		newline_position(p, 1);
+	}
+}
+
+@(private)
+visit_attributes :: proc(p: ^Printer, attributes: [dynamic]^ast.Attribute) {
+
+	if len(attributes) == 0 {
+		return;
+	}
+
+	for attribute, i in attributes {
+		move_line_limit(p, attribute.pos, 1);
+
+		push_generic_token(p, .At, 0);
+		push_generic_token(p, .Open_Paren, 0);
+
+		visit_exprs(p, attribute.elems, true);
+
+		push_generic_token(p, .Close_Paren, 0);
+	}
+}
+
+@(private)
+visit_stmt :: proc(p: ^Printer, stmt: ^ast.Stmt, block_type: Block_Type = .Generic, empty_block := false, block_stmt := false) {
+
+	using ast;
+
+	if stmt == nil {
+		return;
+	}
+
+	switch v in stmt.derived {
+	case Import_Decl:
+		visit_decl(p, cast(^Decl)stmt, true);
+		return;
+	case Value_Decl:
+		visit_decl(p, cast(^Decl)stmt, true);
+		return;
+	case Foreign_Import_Decl:
+		visit_decl(p, cast(^Decl)stmt, true);
+		return;
+	case Foreign_Block_Decl:
+		visit_decl(p, cast(^Decl)stmt, true);
+		return;
+	}
+
+	switch v in stmt.derived {
+	case Using_Stmt:
+		move_line(p, v.pos);
+
+		push_generic_token(p, .Using, 1);
+
+		visit_exprs(p, v.list, true);
+
+		if p.config.semicolons {
+			push_generic_token(p, .Semicolon, 0);
+		}
+	case Block_Stmt:
+		move_line(p, v.pos);
+
+		if v.pos.line == v.end.line {
+			if !empty_block {
+				push_generic_token(p, .Open_Brace, 0);
+			}
+
+			set_source_position(p, v.pos);
+
+			visit_block_stmts(p, v.stmts, len(v.stmts) > 1 && p.config.split_multiple_stmts);
+
+			set_source_position(p, v.end);
+
+			if !empty_block {
+				push_generic_token(p, .Close_Brace, 0);
+			}
+		} else {
+			if !empty_block {
+				visit_begin_brace(p, v.pos, block_type, len(v.stmts));
+			}
+
+			set_source_position(p, v.pos);
+
+			visit_block_stmts(p, v.stmts, len(v.stmts) > 1 && p.config.split_multiple_stmts);
+
+			if !empty_block {
+				visit_end_brace(p, v.end);
+			}
+		}
+	case If_Stmt:
+		move_line(p, v.pos);
+
+		if v.label != nil {
+			visit_expr(p, v.label);
+			push_generic_token(p, .Colon, 0);
+		}
+
+		push_generic_token(p, .If, 1);
+
+		hint_current_line(p, {.If});
+
+		if v.init != nil {
+			p.skip_semicolon = true;
+			visit_stmt(p, v.init);
+			p.skip_semicolon = false;
+			push_generic_token(p, .Semicolon, 0);
+		}
+
+		visit_expr(p, v.cond);
+
+		uses_do := false;
+
+		if check_stmt, ok := v.body.derived.(Block_Stmt); ok && check_stmt.uses_do {
+			uses_do = true;
+		}
+
+		if uses_do && !p.config.convert_do {
+			push_generic_token(p, .Do, 1);
+			visit_stmt(p, v.body, .If_Stmt, true);
+		} else {
+			if uses_do {
+				newline_position(p, 1);
+			}
+
+			set_source_position(p, v.body.pos);
+
+			visit_stmt(p, v.body, .If_Stmt);
+
+			set_source_position(p, v.body.end);
+		}
+
+		if v.else_stmt != nil {
+
+			if p.config.brace_style == .Allman || p.config.brace_style == .Stroustrup {
+				newline_position(p, 1);
+			}
+
+			push_generic_token(p, .Else, 1);
+
+			set_source_position(p, v.else_stmt.pos);
+
+			visit_stmt(p, v.else_stmt);
+		}
+	case Switch_Stmt:
+		move_line(p, v.pos);
+
+		if v.label != nil {
+			visit_expr(p, v.label);
+			push_generic_token(p, .Colon, 0);
+		}
+
+		if v.partial {
+			push_ident_token(p, "#partial", 1);
+		}
+
+		push_generic_token(p, .Switch, 1);
+
+		hint_current_line(p, {.Switch_Stmt});
+
+		if v.init != nil {
+			p.skip_semicolon = true;
+			visit_stmt(p, v.init);
+			p.skip_semicolon = false;
+		}
+
+		if v.init != nil && v.cond != nil {
+			push_generic_token(p, .Semicolon, 0);
+		}
+
+		visit_expr(p, v.cond);
+		visit_stmt(p, v.body);
+	case Case_Clause:
+		move_line(p, v.pos);
+
+		if !p.config.indent_cases {
+			unindent(p);
+		}
+
+		push_generic_token(p, .Case, 0);
+
+		if v.list != nil {
+			visit_exprs(p, v.list, true);
+		}
+
+		push_generic_token(p, v.terminator.kind, 0);
+
+		indent(p);
+
+		visit_block_stmts(p, v.body);
+
+		unindent(p);
+
+		if !p.config.indent_cases {
+			indent(p);
+		}
+	case Type_Switch_Stmt:
+		move_line(p, v.pos);
+
+		hint_current_line(p, {.Switch_Stmt});
+
+		if v.label != nil {
+			visit_expr(p, v.label);
+			push_generic_token(p, .Colon, 0);
+		}
+
+		if v.partial {
+			push_ident_token(p, "#partial", 1);
+		}
+
+		push_generic_token(p, .Switch, 1);
+
+		visit_stmt(p, v.tag);
+		visit_stmt(p, v.body);
+	case Assign_Stmt:
+		move_line(p, v.pos);
+
+		hint_current_line(p, {.Assign});
+
+		visit_exprs(p, v.lhs, true);
+
+		push_generic_token(p, v.op.kind, 1);
+
+		visit_exprs(p, v.rhs, true);
+
+		if block_stmt && p.config.semicolons {
+			push_generic_token(p, .Semicolon, 0);
+		}
+	case Expr_Stmt:
+		move_line(p, v.pos);
+		visit_expr(p, v.expr);
+		if block_stmt && p.config.semicolons {
+			push_generic_token(p, .Semicolon, 0);
+		}
+	case For_Stmt:
+		//this should be simplified
+		move_line(p, v.pos);
+
+		if v.label != nil {
+			visit_expr(p, v.label);
+			push_generic_token(p, .Colon, 0);
+		}
+
+		push_generic_token(p, .For, 1);
+
+		hint_current_line(p, {.For});
+
+		if v.init != nil {
+			p.skip_semicolon = true;
+			visit_stmt(p, v.init);
+			p.skip_semicolon = false;
+			push_generic_token(p, .Semicolon, 0);
+		} else if v.post != nil {
+			push_generic_token(p, .Semicolon, 0);
+		}
+
+		if v.cond != nil {
+			move_line(p, v.cond.pos);
+			visit_expr(p, v.cond);
+		}
+
+		if v.post != nil {
+			push_generic_token(p, .Semicolon, 0);
+			move_line(p, v.post.pos);
+			visit_stmt(p, v.post);
+		} else if v.post == nil && v.cond != nil && v.init != nil {
+			push_generic_token(p, .Semicolon, 0);
+		}
+
+		visit_stmt(p, v.body);
+	case Inline_Range_Stmt:
+
+		move_line(p, v.pos);
+
+		if v.label != nil {
+			visit_expr(p, v.label);
+			push_generic_token(p, .Colon, 0);
+		}
+
+		push_ident_token(p, "#unroll", 0);
+
+		push_generic_token(p, .For, 1);
+
+		hint_current_line(p, {.For});
+
+		visit_expr(p, v.val0);
+
+		if v.val1 != nil {
+			push_generic_token(p, .Comma, 0);
+			visit_expr(p, v.val1);
+		}
+
+		push_generic_token(p, .In, 1);
+
+		visit_expr(p, v.expr);
+		visit_stmt(p, v.body);
+	case Range_Stmt:
+
+		move_line(p, v.pos);
+
+		if v.label != nil {
+			visit_expr(p, v.label);
+			push_generic_token(p, .Colon, 0);
+		}
+
+		push_generic_token(p, .For, 1);
+
+		hint_current_line(p, {.For});
+
+		if len(v.vals) >= 1 {
+			visit_expr(p, v.vals[0]);
+		}
+
+		if len(v.vals) >= 2 {
+			push_generic_token(p, .Comma, 0);
+			visit_expr(p, v.vals[1]);
+		}
+
+		push_generic_token(p, .In, 1);
+
+		visit_expr(p, v.expr);
+
+		visit_stmt(p, v.body);
+	case Return_Stmt:
+		move_line(p, v.pos);
+
+		push_generic_token(p, .Return, 1);
+
+		if v.results != nil {
+			visit_exprs(p, v.results, true);
+		}
+
+		if block_stmt && p.config.semicolons {
+			push_generic_token(p, .Semicolon, 0);
+		}
+	case Defer_Stmt:
+		move_line(p, v.pos);
+		push_generic_token(p, .Defer, 0);
+
+		visit_stmt(p, v.stmt);
+
+		if p.config.semicolons {
+			push_generic_token(p, .Semicolon, 0);
+		}
+	case When_Stmt:
+		move_line(p, v.pos);
+		push_generic_token(p, .When, 1);
+		visit_expr(p, v.cond);
+
+		visit_stmt(p, v.body);
+
+		if v.else_stmt != nil {
+
+			if p.config.brace_style == .Allman {
+				newline_position(p, 1);
+			}
+
+			push_generic_token(p, .Else, 1);
+
+			set_source_position(p, v.else_stmt.pos);
+
+			visit_stmt(p, v.else_stmt);
+		}
+
+	case Branch_Stmt:
+
+		move_line(p, v.pos);
+
+		push_generic_token(p, v.tok.kind, 0);
+
+		if v.label != nil {
+			visit_expr(p, v.label);
+		}
+
+		if p.config.semicolons {
+			push_generic_token(p, .Semicolon, 0);
+		}
+	case:
+		panic(fmt.aprint(stmt.derived));
+	}
+
+	set_source_position(p, stmt.end);
+}
+
+@(private)
+visit_expr :: proc(p: ^Printer, expr: ^ast.Expr) {
+
+	using ast;
+
+	if expr == nil {
+		return;
+	}
+
+	set_source_position(p, expr.pos);
+
+	switch v in expr.derived {
+	case Inline_Asm_Expr:
+		push_generic_token(p, v.tok.kind, 1, v.tok.text);
+
+		push_generic_token(p, .Open_Paren, 1);
+		visit_exprs(p, v.param_types, true, false);
+		push_generic_token(p, .Close_Paren, 0);
+
+		push_generic_token(p, .Sub, 1);
+		push_generic_token(p, .Gt, 0);
+
+		visit_expr(p, v.return_type);
+
+		push_generic_token(p, .Open_Brace, 1);
+		visit_expr(p, v.asm_string);
+		push_generic_token(p, .Comma, 0);
+		visit_expr(p, v.constraints_string);
+		push_generic_token(p, .Close_Brace, 0);
+	case Undef:
+		push_generic_token(p, .Undef, 1);
+	case Auto_Cast:
+		push_generic_token(p, v.op.kind, 1);
+		visit_expr(p, v.expr);
+	case Ternary_Expr:
+		visit_expr(p, v.cond);
+		push_generic_token(p, v.op1.kind, 1);
+		visit_expr(p, v.x);
+		push_generic_token(p, v.op2.kind, 1);
+		visit_expr(p, v.y);
+	case Ternary_If_Expr:
+		visit_expr(p, v.x);
+		push_generic_token(p, v.op1.kind, 1);
+		visit_expr(p, v.cond);
+		push_generic_token(p, v.op2.kind, 1);
+		visit_expr(p, v.y);
+	case Ternary_When_Expr:
+		visit_expr(p, v.x);
+		push_generic_token(p, v.op1.kind, 1);
+		visit_expr(p, v.cond);
+		push_generic_token(p, v.op2.kind, 1);
+		visit_expr(p, v.y);
+	case Selector_Call_Expr:
+		visit_expr(p, v.call.expr);
+		push_generic_token(p, .Open_Paren, 1);
+		visit_exprs(p, v.call.args, true);
+		push_generic_token(p, .Close_Paren, 0);
+	case Ellipsis:
+		push_generic_token(p, .Ellipsis, 1);
+		visit_expr(p, v.expr);
+	case Relative_Type:
+		visit_expr(p, v.tag);
+		visit_expr(p, v.type);
+	case Slice_Expr:
+		visit_expr(p, v.expr);
+		push_generic_token(p, .Open_Bracket, 0);
+		visit_expr(p, v.low);
+		push_generic_token(p, v.interval.kind, 0);
+		if v.high != nil {
+			merge_next_token(p);
+			visit_expr(p, v.high);
+		}
+		push_generic_token(p, .Close_Bracket, 0);
+	case Ident:
+		push_ident_token(p, v.name, 1);
+	case Deref_Expr:
+		visit_expr(p, v.expr);
+		push_generic_token(p, v.op.kind, 0);
+	case Type_Cast:
+		push_generic_token(p, v.tok.kind, 1);
+		push_generic_token(p, .Open_Paren, 0);
+		visit_expr(p, v.type);
+		push_generic_token(p, .Close_Paren, 0);
+		merge_next_token(p);
+		visit_expr(p, v.expr);
+	case Basic_Directive:
+		push_generic_token(p, v.tok.kind, 1);
+		push_ident_token(p, v.name, 0);
+	case Distinct_Type:
+		push_generic_token(p, .Distinct, 1);
+		visit_expr(p, v.type);
+	case Dynamic_Array_Type:
+		visit_expr(p, v.tag);
+		push_generic_token(p, .Open_Bracket, 1);
+		push_generic_token(p, .Dynamic, 0);
+		push_generic_token(p, .Close_Bracket, 0);
+		merge_next_token(p);
+		visit_expr(p, v.elem);
+	case Bit_Set_Type:
+		push_generic_token(p, .Bit_Set, 1);
+		push_generic_token(p, .Open_Bracket, 0);
+
+		visit_expr(p, v.elem);
+
+		if v.underlying != nil {
+			push_generic_token(p, .Semicolon, 0);
+			visit_expr(p, v.underlying);
+		}
+
+		push_generic_token(p, .Close_Bracket, 0);
+	case Union_Type:
+		push_generic_token(p, .Union, 1);
+
+		if v.poly_params != nil {
+			push_generic_token(p, .Open_Paren, 0);
+			visit_field_list(p, v.poly_params, true, false);
+			push_generic_token(p, .Close_Paren, 0);
+		}
+
+		if v.is_maybe {
+			push_ident_token(p, "#maybe", 1);
+		}
+
+		if v.where_clauses != nil {
+			move_line(p, v.where_clauses[0].pos);
+			push_generic_token(p, .Where, 1);
+			visit_exprs(p, v.where_clauses, true);
+		}
+
+		if v.variants != nil && (len(v.variants) == 0 || v.pos.line == v.end.line) {
+			push_generic_token(p, .Open_Brace, 1);
+			visit_exprs(p, v.variants, true);
+			push_generic_token(p, .Close_Brace, 0);
+		} else {
+			visit_begin_brace(p, v.pos, .Generic);
+			newline_position(p, 1);
+			set_source_position(p, v.variants[0].pos);
+			visit_exprs(p, v.variants, true, true);
+			visit_end_brace(p, v.end);
+		}
+	case Enum_Type:
+		push_generic_token(p, .Enum, 1);
+
+		hint_current_line(p, {.Enum});
+
+		if v.base_type != nil {
+			visit_expr(p, v.base_type);
+		}
+
+		if v.fields != nil && (len(v.fields) == 0 || v.pos.line == v.end.line) {
+			push_generic_token(p, .Open_Brace, 1);
+			visit_exprs(p, v.fields, true);
+			push_generic_token(p, .Close_Brace, 0);
+		} else {
+			visit_begin_brace(p, v.pos, .Generic, len(v.fields));
+			newline_position(p, 1);
+			set_source_position(p, v.fields[0].pos);
+			visit_exprs(p, v.fields, true, true, true);
+			set_source_position(p, v.end);
+			visit_end_brace(p, v.end);
+		}
+
+		set_source_position(p, v.end);
+	case Struct_Type:
+		push_generic_token(p, .Struct, 1);
+
+		hint_current_line(p, {.Struct});
+
+		if v.is_packed {
+			push_ident_token(p, "#packed", 1);
+		}
+
+		if v.is_raw_union {
+			push_ident_token(p, "#raw_union", 1);
+		}
+
+		if v.align != nil {
+			push_ident_token(p, "#align", 1);
+			visit_expr(p, v.align);
+		}
+
+		if v.poly_params != nil {
+			push_generic_token(p, .Open_Paren, 0);
+			visit_field_list(p, v.poly_params, true, false);
+			push_generic_token(p, .Close_Paren, 0);
+		}
+
+		if v.where_clauses != nil {
+			move_line(p, v.where_clauses[0].pos);
+			push_generic_token(p, .Where, 1);
+			visit_exprs(p, v.where_clauses, true);
+		}
+
+		if v.fields != nil && (len(v.fields.list) == 0 || v.pos.line == v.end.line) {
+			push_generic_token(p, .Open_Brace, 1);
+			set_source_position(p, v.fields.pos);
+			visit_field_list(p, v.fields, true);
+			push_generic_token(p, .Close_Brace, 0);
+		} else if v.fields != nil {
+			visit_begin_brace(p, v.pos, .Generic, len(v.fields.list));
+			set_source_position(p, v.fields.pos);
+			visit_field_list(p, v.fields, true, true, true);
+			visit_end_brace(p, v.end);
+		}
+
+		set_source_position(p, v.end);
+	case Proc_Lit:
+
+		if v.inlining == .Inline {
+			push_ident_token(p, "#force_inline", 0);
+		}
+
+		visit_proc_type(p, v.type^, true);
+
+		if v.where_clauses != nil {
+			move_line(p, v.where_clauses[0].pos);
+			push_generic_token(p, .Where, 1);
+			visit_exprs(p, v.where_clauses, true);
+		}
+
+		if v.body != nil {
+			set_source_position(p, v.body.pos);
+			visit_stmt(p, v.body, .Proc);
+		} else {
+			push_generic_token(p, .Undef, 1);
+		}
+	case Proc_Type:
+		visit_proc_type(p, v);
+	case Basic_Lit:
+		push_generic_token(p, v.tok.kind, 1, v.tok.text);
+	case Binary_Expr:
+		visit_binary_expr(p, v);
+	case Implicit_Selector_Expr:
+		push_generic_token(p, .Period, 1);
+		push_ident_token(p, v.field.name, 0);
+	case Call_Expr:
+		visit_expr(p, v.expr);
+
+		push_format_token(p, Format_Token {
+			                  kind = .Open_Paren,
+			                  type = .Call,
+			                  text = "(",
+		                  });
+
+		hint_current_line(p, {.Call});
+
+		visit_call_exprs(p, v.args, v.ellipsis.kind == .Ellipsis);
+		push_generic_token(p, .Close_Paren, 0);
+	case Typeid_Type:
+		push_generic_token(p, .Typeid, 1);
+
+		if v.specialization != nil {
+			push_generic_token(p, .Quo, 0);
+			visit_expr(p, v.specialization);
+		}
+	case Selector_Expr:
+		visit_expr(p, v.expr);
+		push_generic_token(p, v.op.kind, 0);
+		visit_expr(p, v.field);
+	case Paren_Expr:
+		push_generic_token(p, .Open_Paren, 1);
+		visit_expr(p, v.expr);
+		push_generic_token(p, .Close_Paren, 0);
+	case Index_Expr:
+		visit_expr(p, v.expr);
+		push_generic_token(p, .Open_Bracket, 0);
+		visit_expr(p, v.index);
+		push_generic_token(p, .Close_Bracket, 0);
+	case Proc_Group:
+
+		push_generic_token(p, v.tok.kind, 1);
+
+		if len(v.args) != 0 && v.pos.line != v.args[len(v.args) - 1].pos.line {
+			visit_begin_brace(p, v.pos, .Generic);
+			newline_position(p, 1);
+			set_source_position(p, v.args[0].pos);
+			visit_exprs(p, v.args, true, true);
+			visit_end_brace(p, v.end);
+		} else {
+			push_generic_token(p, .Open_Brace, 0);
+			visit_exprs(p, v.args, true);
+			push_generic_token(p, .Close_Brace, 0);
+		}
+
+	case Comp_Lit:
+
+		if v.type != nil {
+			visit_expr(p, v.type);
+		}
+
+		if len(v.elems) != 0 && v.pos.line != v.elems[len(v.elems) - 1].pos.line {
+			visit_begin_brace(p, v.pos, .Comp_Lit);
+			newline_position(p, 1);
+			set_source_position(p, v.elems[0].pos);
+			visit_exprs(p, v.elems, true, true);
+			visit_end_brace(p, v.end);
+		} else {
+			push_generic_token(p, .Open_Brace, 1);
+			visit_exprs(p, v.elems, true);
+			push_generic_token(p, .Close_Brace, 0);
+		}
+
+	case Unary_Expr:
+		push_generic_token(p, v.op.kind, 1);
+		merge_next_token(p);
+		visit_expr(p, v.expr);
+	case Field_Value:
+		visit_expr(p, v.field);
+		push_generic_token(p, .Eq, 1);
+		visit_expr(p, v.value);
+	case Type_Assertion:
+		visit_expr(p, v.expr);
+
+		if unary, ok := v.type.derived.(Unary_Expr); ok && unary.op.text == "?" {
+			push_generic_token(p, .Period, 0);
+			visit_expr(p, v.type);
+		} else {
+			push_generic_token(p, .Period, 0);
+			push_generic_token(p, .Open_Paren, 0);
+			visit_expr(p, v.type);
+			push_generic_token(p, .Close_Paren, 0);
+		}
+
+	case Pointer_Type:
+		push_generic_token(p, .Pointer, 1);
+		merge_next_token(p);
+		visit_expr(p, v.elem);
+	case Implicit:
+		push_generic_token(p, v.tok.kind, 1);
+	case Poly_Type:
+		push_generic_token(p, .Dollar, 1);
+		merge_next_token(p);
+		visit_expr(p, v.type);
+
+		if v.specialization != nil {
+			push_generic_token(p, .Quo, 0);
+			merge_next_token(p);
+			visit_expr(p, v.specialization);
+		}
+	case Array_Type:
+		visit_expr(p, v.tag);
+		push_generic_token(p, .Open_Bracket, 1);
+		visit_expr(p, v.len);
+		push_generic_token(p, .Close_Bracket, 0);
+		merge_next_token(p);
+		visit_expr(p, v.elem);
+	case Map_Type:
+		push_generic_token(p, .Map, 1);
+		push_generic_token(p, .Open_Bracket, 0);
+		visit_expr(p, v.key);
+		push_generic_token(p, .Close_Bracket, 0);
+		merge_next_token(p);
+		visit_expr(p, v.value);
+	case Helper_Type:
+		visit_expr(p, v.type);
+	case:
+		panic(fmt.aprint(expr.derived));
+	}
+}
+
+visit_begin_brace :: proc(p: ^Printer, begin: tokenizer.Pos, type: Block_Type, count := 0) {
+
+	set_source_position(p, begin);
+
+	newline_braced := p.config.brace_style == .Allman;
+	newline_braced |= p.config.brace_style == .K_And_R && type == .Proc;
+	newline_braced &= p.config.brace_style != ._1TBS;
+
+	format_token := Format_Token {
+		kind = .Open_Brace,
+		parameter_count = count,
+		text = "{",
+	};
+
+	if newline_braced {
+		newline_position(p, 1);
+		push_format_token(p, format_token);
+		indent(p);
+	} else {
+		format_token.spaces_before = 1;
+		push_format_token(p, format_token);
+		indent(p);
+	}
+}
+
+visit_end_brace :: proc(p: ^Printer, end: tokenizer.Pos) {
+	move_line(p, end);
+	push_generic_token(p, .Close_Brace, 0);
+	unindent(p);
+	p.current_line.depth = p.depth;
+}
+
+visit_block_stmts :: proc(p: ^Printer, stmts: []^ast.Stmt, split := false) {
+	for stmt, i in stmts {
+		visit_stmt(p, stmt, .Generic, false, true);
+
+		if split && i != len(stmts) - 1 && stmt.pos.line == stmts[i + 1].pos.line {
+			newline_position(p, 1);
+		}
+	}
+}
+
+visit_field_list :: proc(p: ^Printer, list: ^ast.Field_List, add_comma := false, trailing := false, enforce_newline := false) {
+
+	if list.list == nil {
+		return;
+	}
+
+	for field, i in list.list {
+
+		if !move_line_limit(p, field.pos, 1) && enforce_newline {
+			newline_position(p, 1);
+		}
+
+		if .Using in field.flags {
+			push_generic_token(p, .Using, 0);
+		}
+
+		visit_exprs(p, field.names, true);
+
+		if field.type != nil {
+			if len(field.names) != 0 {
+				push_generic_token(p, .Colon, 0);
+			}
+			visit_expr(p, field.type);
+		} else {
+			push_generic_token(p, .Colon, 1);
+			push_generic_token(p, .Eq, 0);
+			visit_expr(p, field.default_value);
+		}
+
+		if field.tag.text != "" {
+			push_generic_token(p, field.tag.kind, 1, field.tag.text);
+		}
+
+		if (i != len(list.list) - 1 || trailing) && add_comma {
+			push_generic_token(p, .Comma, 0);
+		}
+	}
+}
+
+visit_proc_type :: proc(p: ^Printer, proc_type: ast.Proc_Type, is_proc_lit := false) {
+
+	if is_proc_lit {
+		push_format_token(p, Format_Token {
+			kind = .Proc,
+			type = .Proc_Lit,
+			text = "proc",
+			spaces_before = 1,
+		});
+	} else {
+		push_format_token(p, Format_Token {
+			kind = .Proc,
+			text = "proc",
+			spaces_before = 1,
+		});
+	}
+
+	explicit_calling := false;
+
+	switch proc_type.calling_convention {
+	case .Odin:
+	case .Contextless:
+		push_string_token(p, "\"contextless\"", 1);
+		explicit_calling = true;
+	case .C_Decl:
+		push_string_token(p, "\"c\"", 1);
+		explicit_calling = true;
+	case .Std_Call:
+		push_string_token(p, "\"std\"", 1);
+		explicit_calling = true;
+	case .Fast_Call:
+		push_string_token(p, "\"fast\"", 1);
+		explicit_calling = true;
+	case .None:
+	//nothing i guess
+	case .Invalid:
+	//nothing i guess
+	case .Foreign_Block_Default:
+	}
+
+	if explicit_calling {
+		push_generic_token(p, .Open_Paren, 1);
+	} else {
+		push_generic_token(p, .Open_Paren, 0);
+	}
+
+	visit_signature_list(p, proc_type.params, false);
+
+	push_generic_token(p, .Close_Paren, 0);
+
+	if proc_type.results != nil {
+		push_generic_token(p, .Sub, 1);
+		push_generic_token(p, .Gt, 0);
+
+		use_parens := false;
+		use_named  := false;
+
+		if len(proc_type.results.list) > 1 {
+			use_parens = true;
+		} else if len(proc_type.results.list) == 1 {
+
+			for name in proc_type.results.list[0].names {
+				if ident, ok := name.derived.(ast.Ident); ok {
+					if ident.name != "_" {
+						use_parens = true;
+					}
+				}
+			}
+		}
+
+		if use_parens {
+			push_generic_token(p, .Open_Paren, 1);
+			visit_signature_list(p, proc_type.results);
+			push_generic_token(p, .Close_Paren, 0);
+		} else {
+			visit_signature_list(p, proc_type.results);
+		}
+	}
+}
+
+visit_binary_expr :: proc(p: ^Printer, binary: ast.Binary_Expr) {
+
+	move_line(p, binary.left.pos);
+
+	if v, ok := binary.left.derived.(ast.Binary_Expr); ok {
+		visit_binary_expr(p, v);
+	} else {
+		visit_expr(p, binary.left);
+	}
+
+	if binary.op.kind == .Ellipsis || binary.op.kind == .Range_Half {
+		push_generic_token(p, binary.op.kind, 0);
+	} else {
+		push_generic_token(p, binary.op.kind, 1);
+	}
+
+	move_line(p, binary.right.pos);
+
+	if v, ok := binary.right.derived.(ast.Binary_Expr); ok {
+		visit_binary_expr(p, v);
+	} else {
+		visit_expr(p, binary.right);
+	}
+}
+
+visit_call_exprs :: proc(p: ^Printer, list: []^ast.Expr, ellipsis := false) {
+
+	if len(list) == 0 {
+		return;
+	}
+
+	//all the expression are on the line
+	if list[0].pos.line == list[len(list) - 1].pos.line {
+		for expr, i in list {
+
+			if i == len(list) - 1 && ellipsis {
+				push_generic_token(p, .Ellipsis, 0);
+			}
+
+			visit_expr(p, expr);
+
+			if i != len(list) - 1 {
+				push_generic_token(p, .Comma, 0);
+			}
+		}
+	} else {
+		for expr, i in list {
+
+			//we have to newline the expressions to respect the source
+			move_line_limit(p, expr.pos, 1);
+
+			if i == len(list) - 1 && ellipsis {
+				push_generic_token(p, .Ellipsis, 0);
+			}
+
+			visit_expr(p, expr);
+
+			if i != len(list) - 1 {
+				push_generic_token(p, .Comma, 0);
+			}
+		}
+	}
+}
+
+visit_signature_list :: proc(p: ^Printer, list: ^ast.Field_List, remove_blank := true) {
+
+	if list.list == nil {
+		return;
+	}
+
+	for field, i in list.list {
+
+		if i != 0 {
+			move_line_limit(p, field.pos, 1);
+		}
+
+		if .Using in field.flags {
+			push_generic_token(p, .Using, 0);
+		}
+
+		named := false;
+
+		for name in field.names {
+			if ident, ok := name.derived.(ast.Ident); ok {
+				//for some reason the parser uses _ to mean empty
+				if ident.name != "_" || !remove_blank {
+					named = true;
+				}
+			} else {
+				//alternative is poly names
+				named = true;
+			}
+		}
+
+		if named {
+			visit_exprs(p, field.names, true);
+
+			if len(field.names) != 0 && field.type != nil {
+				push_generic_token(p, .Colon, 0);
+			}
+		}
+
+		if field.type != nil && field.default_value != nil {
+			visit_expr(p, field.type);
+			push_generic_token(p, .Eq, 1);
+			visit_expr(p, field.default_value);
+		} else if field.type != nil {
+			visit_expr(p, field.type);
+		} else {
+			push_generic_token(p, .Colon, 1);
+			push_generic_token(p, .Eq, 0);
+			visit_expr(p, field.default_value);
+		}
+
+		if i != len(list.list) - 1 {
+			push_generic_token(p, .Comma, 0);
+		}
+	}
+}

+ 1 - 1
core/odin/tokenizer/tokenizer.odin

@@ -608,7 +608,7 @@ scan :: proc(t: ^Tokenizer) -> Token {
 				kind = switch3(t, .And, .And_Eq, '&', .Cmp_And);
 			}
 		case '|': kind = switch3(t, .Or, .Or_Eq, '|', .Cmp_Or);
-		case '~': kind = .Xor;
+		case '~': kind = switch2(t, .Xor, .Xor_Eq);
 		case '<': kind = switch4(t, .Lt, .Lt_Eq, '<', .Shl, .Shl_Eq);
 		case '>': kind = switch4(t, .Gt, .Gt_Eq, '>', .Shr,.Shr_Eq);
 

+ 2 - 2
core/os/file_windows.odin

@@ -273,7 +273,7 @@ is_file :: proc(path: string) -> bool {
 	attribs := win32.GetFileAttributesW(wpath);
 
 	if i32(attribs) != win32.INVALID_FILE_ATTRIBUTES {
-		return attribs & win32.FILE_ATTRIBUTE_DIRECTORY == win32.FILE_ATTRIBUTE_DIRECTORY;
+		return attribs & win32.FILE_ATTRIBUTE_DIRECTORY == 0;
 	}
 	return false;
 }
@@ -283,7 +283,7 @@ is_dir :: proc(path: string) -> bool {
 	attribs := win32.GetFileAttributesW(wpath);
 
 	if i32(attribs) != win32.INVALID_FILE_ATTRIBUTES {
-		return attribs & win32.FILE_ATTRIBUTE_DIRECTORY != win32.FILE_ATTRIBUTE_DIRECTORY;
+		return attribs & win32.FILE_ATTRIBUTE_DIRECTORY != 0;
 	}
 	return false;
 }

+ 216 - 0
tools/odinfmt/flag/flag.odin

@@ -0,0 +1,216 @@
+package flag
+
+import "core:runtime"
+import "core:strings"
+import "core:reflect"
+import "core:fmt"
+import "core:mem"
+import "core:strconv"
+
+Flag_Error :: enum {
+	None,
+	No_Base_Struct,
+	Arg_Error,
+	Arg_Unsupported_Field_Type,
+	Arg_Not_Defined,
+	Arg_Non_Optional,
+	Value_Parse_Error,
+	Tag_Error,
+}
+
+Flag :: struct {
+	optional: bool,
+	type:     ^runtime.Type_Info,
+	data:     rawptr,
+	tag_ptr:  rawptr,
+	parsed:   bool,
+}
+
+Flag_Context :: struct {
+	seen_flags: map[string]Flag,
+}
+
+parse_args :: proc(ctx: ^Flag_Context, args: []string) -> Flag_Error {
+
+	using runtime;
+
+	args := args;
+
+	for true {
+
+		if len(args) == 0 {
+			return .None;
+		}
+
+		arg := args[0];
+
+		if len(arg) < 2 || arg[0] != '-' {
+			return .Arg_Error;
+		}
+
+		minus_count := 1;
+
+		if arg[1] == '-' {
+			minus_count += 1;
+
+			if len(arg) == 2 {
+				return .Arg_Error;
+			}
+		}
+
+		name := arg[minus_count:];
+
+		if len(name) == 0 {
+			return .Arg_Error;
+		}
+
+		args = args[1:];
+
+		assign_index := strings.index(name, "=");
+
+		value := "";
+
+		if assign_index > 0 {
+			value = name[assign_index + 1:];
+			name = name[0:assign_index];
+		}
+
+		flag := &ctx.seen_flags[name];
+
+		if flag == nil {
+			return .Arg_Not_Defined;
+		}
+
+		if reflect.is_boolean(flag.type) {
+			tmp := true;
+			mem.copy(flag.data, &tmp, flag.type.size);
+			flag.parsed = true;
+			continue;
+		} else
+
+		//must be in the next argument
+		if value == "" {
+
+			if len(args) == 0 {
+				return .Arg_Error;
+			}
+
+			value = args[0];
+			args = args[1:];
+		}
+
+		#partial switch _ in flag.type.variant {
+		case Type_Info_Integer:
+			if v, ok := strconv.parse_int(value); ok {
+				mem.copy(flag.data, &v, flag.type.size);
+			} else {
+				return .Value_Parse_Error;
+			}
+		case Type_Info_String:
+			raw_string := cast(^mem.Raw_String)flag.data;
+			raw_string.data = strings.ptr_from_string(value);
+			raw_string.len = len(value);
+		case Type_Info_Float:
+			switch flag.type.size {
+			case 32:
+				if v, ok := strconv.parse_f32(value); ok {
+					mem.copy(flag.data, &v, flag.type.size);
+				} else {
+					return .Value_Parse_Error;
+				}
+			case 64:
+				if v, ok := strconv.parse_f64(value); ok {
+					mem.copy(flag.data, &v, flag.type.size);
+				} else {
+					return .Value_Parse_Error;
+				}
+			}
+		}
+
+		flag.parsed = true;
+	}
+
+	return .None;
+}
+
+reflect_args_structure :: proc(ctx: ^Flag_Context, v: any) -> Flag_Error {
+	using runtime;
+
+	if !reflect.is_struct(type_info_of(v.id)) {
+		return .No_Base_Struct;
+	}
+
+	names := reflect.struct_field_names(v.id);
+	types := reflect.struct_field_types(v.id);
+	offsets := reflect.struct_field_offsets(v.id);
+	tags := reflect.struct_field_tags(v.id);
+
+	for name, i in names {
+		flag: Flag;
+
+		type := types[i];
+
+		if named_type, ok := type.variant.(Type_Info_Named); ok {
+			if union_type, ok := named_type.base.variant.(Type_Info_Union); ok && union_type.maybe && len(union_type.variants) == 1 {
+				flag.optional = true;
+				flag.tag_ptr = rawptr(uintptr(union_type.tag_offset) + uintptr(v.data) + uintptr(offsets[i]));
+				type = union_type.variants[0];
+			} else {
+				return .Arg_Unsupported_Field_Type;
+			}
+		}
+
+		#partial switch _ in type.variant {
+		case Type_Info_Integer, Type_Info_String, Type_Info_Boolean, Type_Info_Float:
+			flag.type = type;
+			flag.data = rawptr(uintptr(v.data) + uintptr(offsets[i]));
+		case:
+			return .Arg_Unsupported_Field_Type;
+		}
+
+		flag_name: string;
+
+		if value, ok := reflect.struct_tag_lookup(tags[i], "flag"); ok {
+			flag_name = cast(string)value;
+		} else {
+			return .Tag_Error;
+		}
+
+		ctx.seen_flags[flag_name] = flag;
+	}
+
+	return .None;
+}
+
+parse :: proc(v: any, args: []string) -> Flag_Error {
+
+	if v == nil {
+		return .None;
+	}
+
+	ctx: Flag_Context;
+
+	if res := reflect_args_structure(&ctx, v); res != .None {
+		return res;
+	}
+
+	if res := parse_args(&ctx, args); res != .None {
+		return res;
+	}
+
+	//validate that the required flags were actually set
+	for k, v in ctx.seen_flags {
+		if v.optional && v.parsed {
+			tag_value: i32 = 1;
+			mem.copy(v.tag_ptr, &tag_value, 4); //4 constant is probably not portable, but it works for me currently
+		} else if !v.parsed && !v.optional {
+			return .Arg_Non_Optional;
+		}
+	}
+
+	return .None;
+}
+
+usage :: proc(v: any) -> string {
+	return "failed";
+}

+ 122 - 0
tools/odinfmt/main.odin

@@ -0,0 +1,122 @@
+package odinfmt
+
+import "core:os"
+import "core:odin/format"
+import "core:fmt"
+import "core:strings"
+import "core:path/filepath"
+import "core:time"
+import "core:mem"
+
+import "flag"
+
+Args :: struct {
+	write: Maybe(bool) `flag:"w" usage:"write the new format to file"`,
+}
+
+print_help :: proc() {
+}
+
+print_arg_error :: proc(error: flag.Flag_Error) {
+	fmt.println(error);
+}
+ 
+format_file :: proc(filepath: string) -> ([]u8, bool) {
+
+	if data, ok := os.read_entire_file(filepath); ok {
+		return format.format(data, format.default_style);
+	} else {
+		return {}, false;
+	}
+}
+
+files: [dynamic]string;
+
+walk_files :: proc(info: os.File_Info, in_err: os.Errno) -> (err: os.Errno, skip_dir: bool) {
+
+	if info.is_dir {
+		return 0, false;
+	}
+
+	if filepath.ext(info.name) != ".odin" {
+		return 0, false;
+	}
+
+	append(&files, strings.clone(info.fullpath));
+
+	return 0, false;
+}
+
+main :: proc() {
+
+	init_global_temporary_allocator(mem.megabytes(100));
+
+	args: Args;
+
+	if len(os.args) < 2 {
+		print_help();
+		os.exit(1);
+	}
+
+	if res := flag.parse(args, os.args[1:len(os.args) - 1]); res != .None {
+		print_arg_error(res);
+		os.exit(1);
+	}
+
+	path := os.args[len(os.args) - 1];
+
+	tick_time := time.tick_now();
+
+	if os.is_file(path) {
+		if _, ok := args.write.(bool); ok {
+			backup_path := strings.concatenate({path, "_bk"}, context.temp_allocator);
+
+			if data, ok := format_file(path); ok {
+
+				os.rename(path, backup_path);
+
+				if os.write_entire_file(path, data) {
+					os.remove(backup_path);
+				}
+			} else {
+				fmt.eprintf("failed to write %v", path);
+			}
+		} else {
+			if data, ok := format_file(path); ok {
+				fmt.println(transmute(string)data);
+			}
+		}
+	} else if os.is_dir(path) {
+		filepath.walk(path, walk_files);
+
+		for file in files {
+			fmt.println(file);
+
+			backup_path := strings.concatenate({file, "_bk"}, context.temp_allocator);
+
+			if data, ok := format_file(file); ok {
+
+				if _, ok := args.write.(bool); ok {
+					os.rename(file, backup_path);
+
+					if os.write_entire_file(file, data) {
+						os.remove(backup_path);
+					}
+				} else {
+					fmt.println(transmute(string)data);
+				}
+			} else {
+				fmt.eprintf("failed to format %v", file);
+			}
+
+			free_all(context.temp_allocator);
+		}
+
+		fmt.printf("formatted %v files in %vms", len(files), time.duration_milliseconds(time.tick_lap_time(&tick_time)));
+	} else {
+		fmt.eprintf("%v is neither a directory nor a file \n", path);
+		os.exit(1);
+	}
+
+	os.exit(0);
+}