From fa2a8643655e0f65ada24ea05c7f8f54163c639e Mon Sep 17 00:00:00 2001 From: Synthasmagoria Date: Sun, 16 Aug 2026 01:14:28 +0200 Subject: [PATCH] Initial commit --- example/example.odin | 75 ++++++++++++++++ example/generate.sh | 1 + example/output.js | 11 +++ main.odin | 209 +++++++++++++++++++++++++++++++++++++++++++ switches.odin | 107 ++++++++++++++++++++++ 5 files changed, 403 insertions(+) create mode 100644 example/example.odin create mode 100755 example/generate.sh create mode 100644 example/output.js create mode 100644 main.odin create mode 100644 switches.odin diff --git a/example/example.odin b/example/example.odin new file mode 100644 index 0000000..b7aa8da --- /dev/null +++ b/example/example.odin @@ -0,0 +1,75 @@ +package xoark_db + +Date :: struct { + year: i16, + month: i8, + day: i8, +} + +Time :: struct { + hour: i8, + minute: i8, + second: i8, +} + +Genre :: enum u8 { + IDM, + Noise, + Glitch, + Breakcore, + SoCalledDnb, + Ambient, + Placeholder01, + Placeholder02, + Placeholder03, + Placeholder04, + Placeholder05, + Placeholder06, + Placeholder07, + Placeholder08, + Placeholder09, + Placeholder10, + Placeholder11, + Placeholder12, + Placeholder13, + Placeholder14, + Placeholder15, + Placeholder16, +} + +Type :: enum u8 { + Album, + EP, + Split, + VA, + Journey, + Single, +} + +@export_js Series :: enum u8 { + None = -2, + Hex, + Base64, + P, + Missing_Broken, + Satellits, + Praspis, + Gnocchi, + Runtime, +}; + +Album :: struct { + genre: bit_set[Genre], + type: Type, + series: Series, + rating: u8, + listened: Date, + released: Date, + duration: Time, + title: string, + art: string, + url: string, + favorite: string, + comment: string, + listens: i32, +} diff --git a/example/generate.sh b/example/generate.sh new file mode 100755 index 0000000..43b311d --- /dev/null +++ b/example/generate.sh @@ -0,0 +1 @@ +odin run ../../odin_js_enum_gen -custom-attribute:"export_js" -- . output.js diff --git a/example/output.js b/example/output.js new file mode 100644 index 0000000..f0d5d7b --- /dev/null +++ b/example/output.js @@ -0,0 +1,11 @@ +const Series = Object.freeze({ + None: -2, + Hex: -1, + Base64: 0, + P: 1, + Missing_Broken: 2, + Satellits: 3, + Praspis: 4, + Gnocchi: 5, + Runtime: 6, +}); diff --git a/main.odin b/main.odin new file mode 100644 index 0000000..9e0a795 --- /dev/null +++ b/main.odin @@ -0,0 +1,209 @@ +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, ""} +} diff --git a/switches.odin b/switches.odin new file mode 100644 index 0000000..a04e9e9 --- /dev/null +++ b/switches.odin @@ -0,0 +1,107 @@ +package main + +import "core:strings" +import "core:fmt" + +SwitchDefinition :: struct { + name: string, + fullname: string, + shortname: string, + usage: string, + description: string, + no_argument: bool, + callback: proc(value: string, data: rawptr) -> (ok: bool), +} + +SwitchSplitError :: enum { + None, + Empty, + MissingDash, + MissingStartQuote, + MissingEndQuote, +} + +switch_split :: proc(str: string) -> (name, value: string, err: SwitchSplitError) { + name = "" + value = "" + err = .None + + if len(str) == 0 { + err = .Empty + return + } + if str[0] != '-' { + err = .MissingDash + return + } + colon_index := strings.index_rune(str, ':') + if colon_index == -1 { + name = str[1:] + return + } + name = str[1:colon_index] + value = str[colon_index + 1:] + if value[0] != '"' && value[len(value) - 1] == '"' { + err = .MissingStartQuote + return + } else if value[0] == '"' && value[len(value) - 1] != '"' { + err = .MissingEndQuote + return + } + if value[0] == '"' && value[len(value) - 1] == '"' { + value = value[1:len(value) - 2] + } + return +} + + +process_switches :: proc(args: []string, switch_definitions: []SwitchDefinition, allocator := context.allocator, data: rawptr) -> bool { + ok := true + error_state: bool + switch_map := make(map[string]SwitchDefinition) + for switch_definition in switch_definitions { + shortname := switch_definition.shortname + shortname_length := len(shortname) + if shortname_length > 0 { + if shortname in switch_map { + ok = false + existing_switch_definition := switch_map[shortname] + fmt.eprintln("Argument error: Switch for ", switch_definition.name, " (", shortname, ") already existed for '", existing_switch_definition.name, sep = "") + } else { + switch_map[shortname] = switch_definition + } + } + + fullname := switch_definition.fullname + fullname_length := len(fullname) + if fullname_length > 0 { + if fullname in switch_map { + ok = false + existing_switch_definition := switch_map[fullname] + fmt.eprintln("Argument error: Switch for ", switch_definition.name, " (", fullname, ") already existed for '", existing_switch_definition.name, sep = "") + } else { + switch_map[fullname] = switch_definition + } + } + + if shortname_length == 0 && fullname_length == 0 { + ok = false + fmt.eprintln("Switch definition error: ", switch_definition.name, " had no shortname or fullname", sep = "") + } + } + for arg in args { + name, value, err := switch_split(arg) + if err != .None { + ok = false + fmt.eprintln("Argument error: ", err, " (", arg, ")", sep = "") + continue + } + if name not_in switch_map { + ok = false + fmt.eprintln("Argument error: Non-existent switch", name, sep = "") + continue + } + switch_map[name].callback(value, data) + } + return ok +}