Best would be to automate this, so I’d say codegen.
As an example, here’s my “project” layout:
.
├── codegen.odin
└── foo
├── file_a.odin
└── file_b.odin
And the files have the following content:
// file_a.odin
package foo
import "core:fmt"
@(codegen_command)
command_1 :: proc() {}
main :: proc() {
for cmd in CMDS {
fmt.println(cmd)
}
}
// file_b.odin
package foo
@(codegen_command)
command_2 :: proc() {}
Note, the custom attribute @(codegen_command)
above both command_1
and command_2
procs.
The actual codegen is (bit messy, I was a bit lazy to clean it up):
package codegen
import "core:odin/parser"
import "core:odin/ast"
import "core:strings"
import "core:os"
import "base:runtime"
import "core:fmt"
main :: proc() {
pkg, ok := parser.parse_package_from_path("./foo")
if !ok {
fmt.println("error: failed to read package 'foo'")
os.exit(1)
}
assert(pkg.kind == .Normal)
Codegen_Command :: struct {
using loc: runtime.Source_Code_Location,
}
cmds: [dynamic]Codegen_Command
n: int
for file_name, file in pkg.files {
for decl in file.decls {
// @(codegen_command) proc_name :: proc() {}
// ^~~~attribute ^~~~name ^~~~value
vd: ^ast.Value_Decl
ok: bool
if vd, ok = decl.derived_stmt.(^ast.Value_Decl); !ok do continue
if vd.is_mutable do continue
if len(vd.values) != 1 do continue
if _, ok = vd.values[0].derived_expr.(^ast.Proc_Lit); !ok do continue
if len(vd.attributes) != 1 do continue
attr_ident := vd.attributes[0].elems[0].derived_expr.(^ast.Ident)
if attr_ident.name != "codegen_command" do continue
proc_ident := vd.names[0].derived_expr.(^ast.Ident)
cmd := Codegen_Command {
file_path = proc_ident.pos.file,
line = cast(i32)proc_ident.pos.line,
column = cast(i32)proc_ident.pos.column,
procedure = proc_ident.name,
}
append(&cmds, cmd)
}
n = len(cmds)
}
sb: strings.Builder
strings.builder_init(&sb)
w := strings.to_writer(&sb)
fmt.wprintln(w, "package foo")
fmt.wprintln(w, "// !!! AUTO GENERATED, DO NOT EDIT !!!")
fmt.wprintln(w, "Command :: struct { name: string, call: proc() }")
fmt.wprintln(w, "CMDS: [dynamic]Command")
fmt.wprintln(w, "@(init) init_commands :: proc() {")
for cmd in cmds {
fmt.wprintfln(w, "\t// generated from %#v", cmd.loc)
fmt.wprintfln(w, "\tappend(&CMDS, Command{{ name = \"%s\", call = %s }})", cmd.procedure, cmd.procedure)
}
fmt.wprintln(w, "}")
ok = os.write_entire_file("./foo/commands_generated.odin", sb.buf[:])
if !ok {
fmt.println("error: failed to generate commands_generated.odin")
os.exit(1)
}
}
Getting to the final executable:
$ odin run codegen.odin -file
$ tree .
.
├── codegen.odin
└── foo
├── commands_generated.odin
├── file_a.odin
└── file_b.odin
2 directories, 4 files
$ odin run foo/ --custom-attribute:codegen_command
Command{name = "command_2", call = proc() @ 0x421310}
Command{name = "command_1", call = proc() @ 0x421320}
And for sake of completeness, here’s the generated file:
package foo
// !!! AUTO GENERATED, DO NOT EDIT !!!
Command :: struct { name: string, call: proc() }
CMDS: [dynamic]Command
@(init) init_commands :: proc() {
// generated from /tmp/scratch/foo/file_b.odin(4:1)
append(&CMDS, Command{ name = "command_2", call = command_2 })
// generated from /tmp/scratch/foo/file_a.odin(6:1)
append(&CMDS, Command{ name = "command_1", call = command_1 })
}