odin_js_enum_gen/main.odin
2026-08-16 01:14:28 +02:00

209 lines
5.6 KiB
Odin

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: <program> <odin module path> <js output file> 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, ""}
}