package main import "core:odin/ast" import "core:odin/parser" import "core:odin/tokenizer" import "core:strings" import "core:strconv" import "core:fmt" import "core:os" concat :: strings.concatenate // TOOD (synthas): Make changeable ATTRIBUTE_NAME :: "export_js" @rodata program_switch_definitions := []SwitchDefinition { { name = "Editor", fullname = "editor", shortname = "e", usage = "", description = "Opens the game in editor mode", no_argument = true, callback = proc(value: string, args: rawptr) -> bool { return true } }, } print_help :: proc() { fmt.println( "--- ODIN JS ENUM GEN ---", "Usage: path", sep = "\n") } main :: proc() { if len(os.args) < 2 { fmt.eprint("Too few arguments") print_help(); return; } odin_module_path := os.args[1] js_output_file_path := os.args[2] if result := program(odin_module_path, js_output_file_path); !result.ok { fmt.eprintln("ERROR ", result.error, ": ", result.message, sep = "") } return } Result :: struct { ok: bool, error: Error, message: string, } Error :: union { ProgramError, os.Error, } ProgramError :: enum { OdinModuleNotExist, TooManyDeclarations, InvalidAttributeUse, CouldntParseEnumValue, EnumValueSmaller, } program :: proc(odin_module_path, js_output_file_path: string) -> Result { if !os.exists(odin_module_path) { message := strings.concatenate({"Module didn't exist at location '", odin_module_path, "'"}, context.temp_allocator) return {false, ProgramError.OdinModuleNotExist, message} } walker := os.walker_create_path(odin_module_path) js := strings.builder_make() for file_info in os.walker_walk(&walker) { if file_info.type == .Directory { walker.skip_dir = true continue } if _, ext := os.split_filename(file_info.name); ext != "odin" { continue } src, error := os.read_entire_file(file_info.fullpath, context.allocator) if error != nil { return {false, error, concat({"Couldn't read file '", file_info.fullpath, "'"})} } file := ast.File{fullpath = file_info.fullpath, src = string(src)} file.derived = &file p: parser.Parser parser.parse_file(&p, &file) if result := generate_enums_from_ast(&file, &js); !result.ok { return result } } if error := os.write_entire_file(js_output_file_path, strings.to_string(js)); error != nil { return {false, error, concat({"Failed to write file to '", js_output_file_path, "'"})} } return {true, nil, ""} } AstWalkerData :: struct { result: Result, js: ^strings.Builder, } ast_pos_to_string :: proc(pos: tokenizer.Pos) -> string { _, filename := os.split_path(pos.file) return fmt.tprint("{'", filename, "', L:", pos.line, ":", pos.column, "}", sep = "") } generate_enums_from_ast :: proc(file: ^ast.File, js: ^strings.Builder) -> Result { data := AstWalkerData{ result = {true, nil, ""}, js = js, } visitor := ast.Visitor { visit = proc(v: ^ast.Visitor, node: ^ast.Node) -> ^ast.Visitor { if node == nil do return v data := cast(^AstWalkerData)v.data #partial switch derived in node.derived { case ^ast.Value_Decl: if !node_value_decl_has_attribute(derived^, ATTRIBUTE_NAME) { return v } if len(derived.values) != 1 { data.result = {false, ProgramError.TooManyDeclarations, ""} return nil } type, is_enum := derived.values[0].derived.(^ast.Enum_Type); if !is_enum { msg := concat({"'", ATTRIBUTE_NAME, "' attribute can only be used on enums ", ast_pos_to_string(node.pos)}) data.result = {false, ProgramError.InvalidAttributeUse, msg} return nil } if result := write_enum_js(type, derived, data.js); !result.ok { data.result = result return nil } return v } return v }, data = &data, } ast.walk(&visitor, file) return data.result } node_value_decl_has_attribute :: proc(node: ast.Value_Decl, attribute_name: string) -> bool { for attribute in node.attributes { for elem in attribute.elems { if ident, is_ident := elem.derived.(^ast.Ident); is_ident && ident.name == attribute_name { return true } } } return false } lit_value_to_string :: proc(expr: ^ast.Expr) -> (text: string, ok: bool) { #partial switch e in expr.derived { case ^ast.Basic_Lit: return e.tok.text, true case ^ast.Unary_Expr: inner, inner_ok := lit_value_to_string(e.expr) if !inner_ok do return "", false return fmt.tprint(e.op.text, inner, sep = ""), true } return "", false } write_enum_js :: proc(type: ^ast.Enum_Type, node: ^ast.Value_Decl, js: ^strings.Builder) -> Result { enum_value := min(i128) fmt.sbprint(js, "const ", node.names[0].derived.(^ast.Ident).name, " = Object.freeze({\n", sep = "") for field in type.fields { #partial switch field in field.derived { case ^ast.Field_Value: ident, is_ident := field.field.derived.(^ast.Ident) if string_value, ok := lit_value_to_string(field.value); ok { if value, ok := strconv.parse_i128(string_value); ok { if value <= enum_value { msg := concat({"Enum values smaller than previous are not supported ", ast_pos_to_string(node.pos)}) return {false, ProgramError.EnumValueSmaller, msg} } enum_value = value fmt.sbprint(js, "\t", ident.name, ": ", enum_value, ",\n", sep = "") enum_value += 1 } else { msg := concat({"Couldn't parse value of enum member' ", ident.name, "' ", ast_pos_to_string(node.pos)}) return {false, ProgramError.CouldntParseEnumValue, msg} } } case ^ast.Ident: if enum_value == min(i128) { enum_value = 0 } fmt.sbprint(js, "\t", field.name, ": ", enum_value, ",\n", sep = "") enum_value += 1 } } fmt.sbprint(js, "});\n", sep = "") return {true, nil, ""} }