jow- / ucode

JavaScript-like language with optional templating
ISC License
87 stars 24 forks source link

call() scope does not override imported symbols #116

Open huafu opened 1 year ago

huafu commented 1 year ago

I've wrote a Typescript transformer for ucode as well as typings so that one can write ucode code with strict typings. I've also wrote a test library with spies and stubs so that ucode projects can have unit tests also written in Typescript. it's going to be on my public Github once ready (or at least when in a more advanced stage). For the spies and stubs, I'm using the call function which is pretty handy. Tho there is one blocking issue:

// my_lib.uc
import { to_stub } from './other_lib.uc';

function fn_to_test() {
  // ...
  to_stub("hello");
  // ...
}

export { fn_to_test };
// my_lib.spec.uc
call(fn_to_test, { to_stub: () => null });

here I was expecting fn_to_test to use the dummy () => null when calling to_stub instead of the one imported from my_lib.uc. I guess it's the normal behavior, tho it'd be nice if the scope given to call() could override imported symbols as well as globals.

jow- commented 1 year ago

Imported symbols act like local variables, that's why you can't shadow them by overriding the scope (once the compiled bytecode runs, accessing imported symbols is not a property lookup in the global scope but a direct stack slot / upvalue access, basically the same as a local variable).

Without introducing a set of debug/introspection APIs, the only solution I can think of is compiling and loading the code to test at runtime using loadfile() / loadstring() and modifying module_search_path to inject a path with mock libraries before the standard library search paths. This will then cause the dynamically loaded code to get compiled against a set of stub libraries instead of the actual ones.

If that is not an option, then we will need a kind of debug library, comparable to the Lua one, which allows getting and setting the bound upvalues of a function in order to patch other values in there.

huafu commented 1 year ago

If that is too complex, then I guess I'll have to improve my Typescript transformer (to make it transparent) or my test lib (to make it work also without TS) so that it loads functions to test not directly but thru a helper. This would then use module_search_path of loadstring() as you suggested to override imported stuff.

huafu commented 1 year ago

Is it normal that I can't shadow defined constants as well?

// lib.uc
const to_stub = () => "original"
function fn_to_test() {
  return to_stub();
}
export { fn_to_test };
// lib.spec.uc
import { fn_to_test } from './lib.uc';
print(call(fn_to_test, { to_stub: () => "override" })); // prints "oiriginal"
jow- commented 1 year ago

Yes, those are local variables too.

huafu commented 1 year ago

Imported symbols act like local variables

Oh, I thought you meant local variables in the function scope, not in the global scope (ie the one holding the function). Now I understand :smile:

huafu commented 1 year ago

@jow- as I mentioned before, I've just published the draft of my TypeScript to ucode compiler there - you get typings, classes and code completion for ucode thanks to typescript and some transformers. Plus the code is going thru eslint to ensure no ; or other stuffs of that kind are forgotten. A lot more work to be done there, but it does work and having some input on what could be improved will be very welcome ;-)