wren-lang / wren

The Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.
http://wren.io
MIT License
6.9k stars 552 forks source link

Add objects to to the root module, like System. #630

Closed IngwiePhoenix closed 5 years ago

IngwiePhoenix commented 5 years ago

I would like to make a few things from my application available to my users immediately, in every file, without having to import "the module" for vars. :)

Now, jokes aside, I looked up how System is registered, and came across this line: https://github.com/munificent/wren/blob/master/src/vm/wren_core.c#L1146-L1147

How would I go about, possibly without having to directly include the related headers and directly interact with the stack - so Wren doesn't suddenly act awkward due to a mistake I made - adding a "root" object?

For instance, I want to make a File class available immediately without having to import it each and every time that I use it.

ruby0x1 commented 5 years ago

The core module is loaded from a wren file, which you can see here, the c part just hooks up the bindings to the functions and types as needed. Some have bindings, some are only wren.

Note that the wren file is used as a reference, and then generated into this c file that gets compiled in. So if you just change the wren source, it won't show up. You'd have to make sure the inc is updated, which i think is in the python somewhere (I've haven't needed to change that yet).

Another (maybe less ideal) approach is to pre-concatenate the loaded module source, at import time. Since you have the control there, you can just do return "import "file" for File" + originalSource in your loadModuleFn callback.

IngwiePhoenix commented 5 years ago

So if I changed (and then re-generated) that wren file, I would be able to add to the core module, which is loaded every time? Or can I also just use the fact that the module ID is NULL and put my stuff there?

I guessed as far with that Wren file, I just finished reading it actually. There are way more classes in there than I had actually known...

ruby0x1 commented 5 years ago

There's a distinction between how the core module is handled vs regular foreign functions though, the PRIMITIVE vs FOREIGN distinction among other things. I'd be wary of changing it that way first. If you just want an automatic import, there's simpler ways, like the string splicing, or defining variables in the module.

If you're willing to change the core module, then you can also just define variables inside the load function by giving yourself a second callback in the config, and calling out to yourself with the module from inside the vm code. From there you can manually import whatever you want after the fact too, using the api only.

IngwiePhoenix commented 5 years ago

I see. Well I guess all that's left for me is to figure out a good way to do so.

Though, there really should be - in my opinion - a way for users to define "always present" classes and variables without having to go as far as to modifying the core's actual files.

Though, would you may elaborate on what exactly you meant with:

If you just want an automatic import, there's simpler ways, like the string splicing, or defining variables in the module.

If I use wrenInterpret on the main module - as in, the first one that I will be loading - it will not affect any other modules in the variables and classes they have. At least, that is how I had understood it so far. And, do you mean that I prepend some source before any other source that I load with what you called "string splicing"? I am not a native speaker - so "splicing" is confusing me a little.

ruby0x1 commented 5 years ago

No problem, so let's look at what happens when you load a module:

Now, we can see that loadModuleFn can modify the source (in your own code) before returning it to wren. This loadModuleFn is set on the config when configuring the VM, before creating it. The code isn't really functional below, it's just to illustrate.

static char* loadModule(WrenVM* vm, const char* module) {
   //embedder specific
  const char* module_source = load_file(module);
  const char* default_imports = "import \"file\" for File\n"
 ...
  const char* result = strcat(default_imports, module_source);
  return result;
}

Here, we just joined the two strings together, before giving it to the virtual machine to compile. Now every module ever, has the File variable already defined. No vm/core changes.

The second method is to change the vm. I would start with the easier path above.

mhermier commented 5 years ago

Minor issue with that solution, it will mess line numbering.

IngwiePhoenix commented 5 years ago

It appears that this is pretty much the "safest" option out there, as I am not fond of messing with the core. Yes, it may end up messing around with the line numbers, but I guess there is no other option - yet.

Thank you very much for taking the time to help! :)

ruby0x1 commented 5 years ago

Yea the lines would be off, good to note. One step up to changing the vm, can add compileInModule inside the wrenImportModule function with your own imports, which wouldn't change lines.

My earlier suggestion was to add an additional callback. There is loadModuleFn, you could add postLoadModuleFn and do any extern stuff in your own code, with minimal vm change. Either way should work for now.

IngwiePhoenix commented 5 years ago

I have accidentially encountered an even less issue-y thing: Calling wrenInterpret two consecutive times does not mess up the lines AND runs in the same context, when module matches. See #631 - I edited the code some more, and actually found that out. I dont know if that is intended, but it works - without breaking line numbers! :)

IngwiePhoenix commented 5 years ago

This one will correctly throw an error on internal:main:3 - and if you remove the false statement on that line, it will print the proper value of x.

#include <stdio.h>
#include "wren.h"

void printOut(WrenVM* vm, const char* text) {
  printf("%s", text);
}

void printErr(
    WrenVM* vm, WrenErrorType type,
    const char* module, int line,
    const char* message
) {
  switch(type) {
    case WREN_ERROR_RUNTIME:
      printf("[wren] ERR: Runtime reported: \"%s\"\n", message);
      return;
    case WREN_ERROR_COMPILE:
      printf("[wren] ERR: (%s:%i) %s\n", module, line, message);
      return;
    case WREN_ERROR_STACK_TRACE:
      printf("[wren] STACK: %s:%i = %s", module, line, message);
      return;
  }
}

const char* rootCode =
"class A {\n"
"  construct new() { System.print(\"Constructed\") }\n"
"}\n"
"var x = 123";

const char* script =
"var a = A.new()\n"
"System.print(x)\n"
"nonExistantFunction()";

const char* rootModule = "internal:main";

int main(int argc, const char** argv) {
  WrenVM* vm;
  WrenConfiguration config;
  wrenInitConfiguration(&config);

  config.writeFn = printOut;
  config.errorFn = printErr;

  vm = wrenNewVM(&config);

  WrenInterpretResult rt;

  printf("Root: \n");
  rt = wrenInterpret(vm, rootModule, rootCode);
  if(rt != WREN_RESULT_SUCCESS) exit(1);

  printf("Main: \n");
  rt = wrenInterpret(vm, rootModule, script);
  if(rt != WREN_RESULT_SUCCESS) exit(1);

  wrenFreeVM(vm);
  exit(0);
}

And with that...all I need is to sneak a wrenInterpret call to before the actual source is being executed. Probably in loadModuleFn?

const char* loadModuleFn(/* ... */) {
  // Find source, store it into src.
  const char* src;

  // Call wrenInterpret before returning, maybe?
  wrenInterpret(vm, module, preSrc);
  return src;
}

You think this would work?

IngwiePhoenix commented 5 years ago

I went ahead and tried it - it works.

// Dummy method. Only ever returns one, plain module.
char* loadModuleFn(WrenVM* vm, const char* module) {
  printf("> Dummy loaded: %s\n", module);
  char* dumbSrc = "System.print(\"Pre module code\")";
  char* dumbSrc_ptr = (char*)malloc(strlen(dumbSrc)+1);
  strcpy(dumbSrc_ptr, dumbSrc_ptr);
  wrenInterpret(vm, module, dumbSrc);
  char* src = "var MyName = \"Ingwie\"";
  char* src_ptr = (char*)malloc(strlen(src)+1);
  strcpy(src_ptr, src);
  return src_ptr;
}

Yes... my memory management is lacky - but I was just trying to hack together a solution that'll work for the meantime of the test. :)

mhermier commented 5 years ago

A little bit hacky way would be to create a preamble callback and a function to declare objects. This is how core object does, could be done in ~15 lines of code I think, if one could get Wren handle to inject.