d5/tengo

Dynamically generated code for import

bfreis opened this issue · 0 comments

Hi!

While using tengo embedded into some code, I wanted to make it possible to dynamically provide source code for imports, without having to import files from disk. Specifically, the import names I want to use do not map to files on disk, and are unknown before compiling a script (so I couldn't even somehow pre-generate them anywhere).

I couldn't find any easy way to do it, just a very ugly hack, based on trying to compile on a loop, handling any CompilerError on ImportExpr by generating the module I need, adding it to a ModuleMap, and trying again. Something like this:

moduleMap := tengo.NewModuleMap()
script := tengo.NewScript(src)
script.SetImports(moduleMap)

var c *tengo.Compiled
var err error

done := false
for !done {
  done = true
  c, err = script.Compile()
  var cerr *tengo.CompilerError
  if errors.As(err, &cerr) {
    if node, ok := cerr.Node.(*parser.ImportExpr); ok {
      mSrc := generateModuleSource(node.ModuleName)
      moduleMap.AddSourceModule(node.ModuleName, mSrc)
      err = nil
      done = false
    }
  }
}

if err != nil { ... }

// use c here

But this is beyond ugly.

There's a rather simple alternative: if we extract a trivial interface from *tengo.ModuleMap with just its Get method, and change a few places that declare parameters or fields with the new interface, I can pass an interface instead, and do my dynamic generation there.

E.g.:

// (some changes omitted for brevity)

// tengo: modules.go:
type ModuleGetter interface {
  Get(name string) Importable
}

// tengo: script.go:
func (s *Script) SetImports(modules ModuleGetter) {
	s.modules = modules
}

// my_code.go:
type DynamicModuleGetter { ... }
func (d *DynamicModuleGetter) Get(name string) tengo.Importable {
  src := ...
  return &tengo.SourceModule{Src: src}
}
// ...
m := &DynamicModuleGetter{...}
script := tengo.NewScript(src)
script.SetImports(m)
c, err := script.Compile()
if err != nil { ... }
// use c here

This has the added benefit of getting the code closer to Go's good practice of taking interfaces instead of concrete types.

Will send a PR soon.

Thoughts?