Get procedure metadata natively?

Briefly: I’m building a CLI tool and want to allow procedures to be called interactively from the tool by their name. Obviously I can do this naively, but I want to avoid having multiple places to fix if these procedures change during a refactor.

Is there some way I can extract all the procedure names from a package?

What I’d like to do is something like this (so I can manage the array right next to the associated procedures):

Command :: struct {
	name      : string,
	procedure : proc(),
}
procs := make([dynamic]Command)
append(&procs, Command{"a", a})
a :: proc() {
    ...
}

But of course I can’t put runtime expressions at the file scope.

I know I can centralise all these in a proc somewhere, but the goal is to keep the associated operation right next to the proc so refactoring is simpler.

1 Like

New to Odin, but the naïve approach, isn’t that the Odin approach?

All things in a package is in the same scope by default.
Unless you mark procs or files private.
Odin doesn’t really care whether you have everything in one or in 42 files.

If you really need to, but it’s not Odinesque, you can use folders… See the FAQ/overview about packages.
And watch one of the many videos where we hear from the One about packages :package::package:

I would just forget about being clever and just code on. It’s hard to resist going down the architecture rabbit hole, and say to hell with it and start being productive.
There’s a time to be clever, and that is usually later rather than sooner :wink:

The above is mainly aimed at myself :melting_face:

The naive way is not very good; it requires modifying two files for every little refactor.

If a struct was available that listed every proc in a package the solution would be very simple. In theory I don’t see an issue with it, since the procedures in a package should be known at compile time. You hinted this is possible but you didn’t really explain how, and after scouring docs I don’t think it is.

I have no issue accepting that my request here doesn’t fit Odin overall due to a nuanced trade off.

I don’t think that means this is just secretly a feature I shouldn’t build because it requires a little extra work than it might in another language that makes a different trade off, or that I’m getting ahead of myself.

1 Like

There is something somewhere that does that. I am not sure what or where, but Odin manages to do it each time it builds and runs our code.
There are people who have created an LSP server, and people who have written editors in various states of completion before we got that LSP server named OLS.

Since I obviously don’t know enough to point you in a more accurate direction, I think that you would be well served by visiting the Discord and ask there about the specific things you need from Odin :slightly_smiling_face:

EDIT:
There is the core/odin package group (?) with ast, lexer and so on → package odin/ast - pkg.odin-lang.org

Here’s OLS:

Here’s the entry in Odin Showcase for OLS:

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 })
}
2 Likes

Good to know I wasn’t completely out to lunch with my scanty advice!

1 Like

Thanks for sharing! That’s a great example for how to use the parser, and it looks like you’re example is very similar to my goals.

I had mostly given up on an easy solution (one that didn’t require me manually parsing or learning another library) and just exporting arrays and using the text editor to search and replace - the meta-program is certainly more general and you’ve proven fairly simple to implement.

I nominate (with no power or authority) this to be added to the code generation example.

1 Like