This prevents a memleak, noticeable when running `wren_test` under
`valgrind`. For example, the following command would leak
`./bin/wren_test_d any_example.wren`
* Optimize Random.sample(_, _) for performance
* Make tests treat random samples as unordered
* Test all sample sizes possible
* Tweak random sampling algorithm for performance
hashBits() is used to generate a hash code from the same 64 bits used
to represent a Wren number as a double. When building a map containing
a large number of integer keys, it's important for this to do a good
job scattering the bits across the 32-bit key space.
Alas, it does not. Worse, the benchmark to test this happens to stop
just before the performance falls off a cliff, so this was easy to
overlook.
This replaces it with the hash function V8 uses, which has much better
performance across the numeric range.
This doesn't let you arbitrarily call back into the VM from within
foreign methods. I'm still not sure if that's even a good idea since
God knows what that would mean if you switch fibers while doing that.
But this does allow the very important use case of being able to call
a foreign method from within a call to wrenCall(). In other words,
foreign methods need to always be leaf calls on the call stack, but the
root of that stack can now come from runInterpreter() or wrenCall().
Fix#510.
The VM used to not detect this case. It meant you could get into a
situation where another fiber's caller had completed. Then, when it
tried to resume that fiber, the VM would crash because there was nothing
to resume to.
This is part of thinking through all the cases around re-entrancy. Added
some notes for that too.
There's a lot of changes here and surely some rough edges to iron out.
Also, I need to update the docs. But I want to get closer to landing
this so I can build on it.
A statement like:
for (i in 1..2)
When run in the REPL declares a local variable ("i"), but not inside
a function or method body. This hit a corner case in the compiler
where it didn't have the correct slot indexes set up.
That corner case is because sometimes when you compile a chunk, local
slot zero is pre-allocated -- either to refer to "this" or to hold the
closure for a function so that it doesn't get GCed while running. But
if you're compiling top-level code, that slot isn't allocated. But top
level code for the REPL *should* be, because that gets invoked like a
function.
To simplify things, *every* compiled chunk now pre-allocates slot zero.
That way, there are fewer cases to keep in mind.
Also fixed an issue where a GC during an import could collected the
imported module body's closure.
Fix#456.
This is a breaking change because existing imports in user Wren code
that assume the path is relative to the entrypoint file will now likely
fail.
Also, stack trace output and host API calls that take a module string
now need the resolved module string, not the short name that appears in
the import.
This is just for the VM's own internal use, for resolving relative
imports.
Also added a tiny unit test framework for writing tests of low-level
C functionality that isn't exposed directly by the language or VM.
This is an interim step towards supporting relative imports. Previously,
the IMPORT_VARIABLE instruction had a constant string operand for the
import string of the module to import the variable from. However, with
relative imports, the import string needs to be resolved by the host
all into a canonical import string. At that point, the original import
string in the source is no longer useful.
This changes that to have IMPORT_VARIABLE access the imported ObjModule
directly. It works in two pieces:
1. When a module is compiled, it ends with an END_MODULE instruction.
That instruction stores the current ObjModule in vm->lastModule.
2. The IMPORT_VARIABLE instruction uses vm->lastModule as the module to
load the variable from. Since no interesting code can execute between
when a module body completes and the subsequent IMPORT_VARIABLE
statements, we know vm->lastModule will be the one we imported.
This is simpler and marginally faster. We don't need the overhead of
fibers since you can't have long or recursive import chains anyway.
More importantly, this makes the behavior more well-defined when you do
things like yield from an imported module. (Not that you should do that,
but if you do, it shouldn't do weird things.)
If the function the fiber is created from takes a parameter, the value
passed to the first call() or transfer() gets bound to that parameter.
Also, this now correctly handles fibers with functions that take
parameters. It used to leave the stack in a busted state. Now, it's a
runtime error to create a fiber with a function that takes any more
than one parameter.
These lazy iterator producing methods are useful when working with
arbitrary sequences and you need to skip or take some number of elements
at the start.
They were inadvertently relying on undefined behavior in C and we get
different results on some compilers.
Until we decide how we want the operation to behave, for now, just
leave it unspecified.