├── .github └── workflows │ └── runTests.yml ├── LICENSE ├── README.md ├── TODO.md ├── euwren.nimble ├── src ├── euwren.nim └── euwren │ └── private │ └── wren.nim └── tests ├── .gitignore ├── config.nims └── teuwren.nim /.github/workflows/runTests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | name: Test on ubuntu-latest with Nim ${{ matrix.nim_version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | nim_version: ["1.0.0", "1.0.2", "1.0.4", "stable"] 12 | 13 | steps: 14 | - name: Checkout euwren 15 | uses: actions/checkout@v1 16 | 17 | - name: Cache ~/.choosenim 18 | id: cacheChoosenim 19 | uses: actions/cache@v1 20 | with: 21 | path: ~/.choosenim 22 | key: ${{ runner.os }}-choosenim-${{ matrix.nim_version }} 23 | - name: Cache ~/.nimble 24 | id: cacheNimble 25 | uses: actions/cache@v1 26 | with: 27 | path: ~/.choosenim 28 | key: ${{ runner.os }}-choosenim-${{ matrix.nim_version }} 29 | 30 | - name: Setup Nim environment 31 | uses: jiro4989/setup-nim-action@v1.0.1 32 | with: 33 | nim-version: ${{ matrix.nim_version }} 34 | if: > 35 | steps.cacheChoosenim.outputs.cache-hit != true && 36 | steps.cacheNimble.outputs.cache-hit != true 37 | 38 | - name: Run all tests 39 | run: | 40 | nimble -y install 41 | nim c -r tests/teuwren 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 lqdev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # euwren 2 | 3 | > The [Eurasian wren](https://en.wikipedia.org/wiki/Wren) has been long 4 | > considered "the king of birds" in Europe. 5 | 6 | euwren (pronounced _oyren_, like _euro_ in German) is a high-level 7 | [Wren](https://github.com/wren-lang/wren) wrapper for Nim. Wren is a small, 8 | fast, embedded scripting language. 9 | 10 | The main point of euwren is to create a very user-friendly, high-level wrapper: 11 | "The king of Wren wrappers". It leverages Nim's powerful macro system to make 12 | the API as simple as listing all the things you need in Wren. While it may not 13 | be the fastest of wrappers, it's not the primary goal. It's the end user 14 | experience that really counts. 15 | 16 | ## Features 17 | 18 | - Syntactically simple 19 | - Supports proc, object, and enum binding 20 | - Does type checks for procedures 21 | - Supports operator overloading 22 | - Automatically generates Wren glue code with declarations 23 | 24 | ## Installing 25 | 26 | ### Adding to your .nimble file 27 | ```nim 28 | requires "euwren" 29 | ``` 30 | 31 | ### Installing directly from the command line 32 | ```bash 33 | $ nimble install euwren 34 | ``` 35 | 36 | ## Usage 37 | 38 | Because Nim and Wren have different programming paradigms, some work must be 39 | done by the programmer. Fortunately, what needs to be done is very simple, so 40 | don't worry. 41 | 42 | ### Running code 43 | 44 | First, a VM instance must be created. 45 | ```nim 46 | import euwren 47 | 48 | var wren = newWren() 49 | ``` 50 | After that, running code is as simple as: 51 | ```nim 52 | # run() runs code in the 'main' module 53 | # it's an alias to wren.module("main", ) 54 | wren.run(""" 55 | System.print("Hello from Wren!") 56 | """) 57 | ``` 58 | 59 | ### Retrieving variables 60 | 61 | To get a primitive variable from Wren, use the subscript operator with three 62 | arguments. 63 | ```nim 64 | wren.run(""" 65 | var myInt = 2 66 | """) 67 | 68 | # module name type 69 | let myInt = wren["main", "myInt", int] 70 | assert myInt == 2 71 | ``` 72 | 73 | Any number/enum type conversions between Nim and Wren are performed 74 | automatically. 75 | 76 | To retrieve a Wren object, eg. a class, use the subscript operator with two 77 | arguments. 78 | ```nim 79 | wren.run(""" 80 | class Program { 81 | static run() { 82 | System.print("Hello from inside the class!") 83 | } 84 | } 85 | """) 86 | 87 | let classProgram = wren["main", "Program"] 88 | ``` 89 | 90 | ### Calling methods 91 | 92 | Calling methods on Wren objects is done by first obtaining a call handle, and 93 | then calling the method. 94 | 95 | To obtain a call handle, use the curly brace operator. Then, to call the 96 | method, use `call()`. 97 | ```nim 98 | # the convention for naming the variable: 99 | # method 100 | # this convention is the preferred naming conventions for variables and fields 101 | # that store Wren call handles, but you're free to use any convention you want 102 | let methodRun0 = wren{"run()"} 103 | # the second parameter is the call handle to the method, the third is the 104 | # receiver of the method, and the rest is the parameters to pass to 105 | # the method. 106 | # when the method is static, the receiver is the class of the method 107 | wren.call(methodRun0, classProgram) 108 | ``` 109 | 110 | ### Configuring the VM 111 | 112 | The VM can be configured in a few different ways, presented in following 113 | paragraphs. 114 | 115 | #### Redirecting output 116 | 117 | By default, the VM writes to `stdout`. Sometimes (eg. in a game), you may have 118 | a dedicated GUI console, and you'd like to redirect the VM output there. 119 | 120 | To do this, you can set the `onWrite` callback. 121 | ```nim 122 | var vmOut = "" 123 | 124 | wren.onWrite do (text: string): 125 | # ``text`` contains the text the VM wants to output. 126 | # let's redirect it to ``vmOut``. 127 | vmOut.add(text) 128 | 129 | wren.run(""" 130 | System.print("Testing output!") 131 | """) 132 | assert vmOut == "Testing output!\n" 133 | ``` 134 | 135 | #### Controlling imports 136 | 137 | By default, `import` cannot be used in scripts. It will throw an error, because 138 | there is no default implementation for imports. 139 | 140 | To make imports work, you must set the `onLoadModule` callback in the VM: 141 | ```nim 142 | import os 143 | 144 | wren.onLoadModule do (name: string) -> string: 145 | # onLoadModule must return the source code of the module called ``name``. 146 | # a usual implementation that'd allow for loading from files would look 147 | # like so: 148 | result = readFile(name.addFileExt("wren")) 149 | ``` 150 | 151 | Sometimes, you may want to transform the module name before importing. That's 152 | where `onResolveModule` comes in: 153 | ```nim 154 | import os 155 | 156 | wren.onResolveModule do (importer, name: string) -> string: 157 | # this is a standard way of implementing relative imports: 158 | result = importer/name 159 | ``` 160 | Apart from this, if your callback returns an empty string, an error will be 161 | raised saying that the module does not exist. 162 | ```nim 163 | import os 164 | 165 | wren.onResolveModule do (importer, name: string) -> string: 166 | result = importer/name 167 | if not fileExists(result.addFileExt("wren")): 168 | result = "" # module does not exist. 169 | ``` 170 | 171 | ### Binding procs 172 | 173 | Wren is strictly class-based, but Nim is not—that means that any procs passed to 174 | Wren must be nested inside a class. Fortunately, that's pretty simple. 175 | 176 | ```nim 177 | proc hello() = 178 | echo "Hello from Nim!" 179 | 180 | wren.foreign("nim"): 181 | # create a namespace 'Nim' that will hold our proc 182 | [Nim]: 183 | # bind the proc 'hello' 184 | hello 185 | # ready() must be called to ready the VM for code execution after any 186 | # foreign() calls. this arms the VM to do code execution with foreign type 187 | # checking. no calls to foreign() should be done after you call this! 188 | wren.ready() 189 | ``` 190 | ```js 191 | import "nim" for Nim 192 | Nim.hello() // Output: Hello from Nim! 193 | ``` 194 | Here's a more advanced example: 195 | ```nim 196 | proc add(a, b: int): int = a + b 197 | proc add(a, b, c: int): int = a.add(b).add(c) 198 | proc subtract(a, b: int): int = a - b 199 | 200 | # foreign() accepts the name of the module we want to bind 201 | wren.foreign("math"): 202 | # we create a namespace 'Math' for our procs 203 | [Math]: 204 | # procs can be overloaded by arity, but not by parameter type 205 | # (this is not enforced, so be careful!) 206 | add(int, int) 207 | add(int, int, int) 208 | # procs can be aliased on the Wren side 209 | subtract -> sub 210 | wren.ready() 211 | ``` 212 | ```js 213 | import "math" for Math 214 | System.print(Math.add(2, 2)) // Output: 4 215 | ``` 216 | 217 | If a proc has a parameter without a type, but with a default parameter that is 218 | not a literal, you must use `_`: 219 | ```nim 220 | var myNumber = 2 221 | proc annoyingProc(a = myNumber) = discard 222 | 223 | wren.foreign("wildcard"): 224 | [Wildcard]: 225 | annoyingProc(_) 226 | ``` 227 | This is due to a limitation in Nim. For some reason, default parameters are 228 | untyped, which doesn't let the overload resolution compare the types properly. 229 | This is fine, unless you use a non-literal type for the default parameter, eg. 230 | a variable or `seq`. You don't have to do this if the type is specified 231 | explicitly, like here: 232 | ```nim 233 | proc notSoAnnoyingProc(a: int = myNumber) = discard 234 | ``` 235 | 236 | New procedures may be created directly within a class binding. This is referred 237 | to as an *inline* declaration: 238 | ```nim 239 | wren.foreign("inline"): 240 | [Inline]: 241 | sayHi do (): 242 | # note that ``do ():`` **must** be used in this case, since ``do:`` is 243 | # just syntax sugar over a regular ``:`` block. regular blocks are 244 | # reserved for parameter type-based overloads, which may be implemented 245 | # in the future. 246 | echo "Hi!" 247 | # getters can also be declared this way, and accept no parameters 248 | ?pi do -> float: 3.14159265 249 | ``` 250 | 251 | Any exceptions raised from Nim procedures will abort the fiber instead of 252 | crashing the program: 253 | ```nim 254 | proc oops() = 255 | raise newException(Defect, "oops! seems you called oops().") 256 | 257 | wren.foreign("errors"): 258 | [Error]: 259 | oops 260 | wren.ready() 261 | ``` 262 | ```js 263 | import "errors" for Error 264 | // we can use the standard Wren error handling mechanisms to catch our error. 265 | var theError = Fiber.new { 266 | Error.oops() 267 | }.try() 268 | System.print(theError) 269 | ``` 270 | ``` 271 | oops! seems you called oops(). [Defect] 272 | ``` 273 | When `compileOption("stacktrace") == on`, a stack trace pointing to 274 | where the error was raised will be printed. 275 | 276 | Nim procedures can accept `WrenRef` as arguments. This allows Wren objects to 277 | be passed to Nim: 278 | ```nim 279 | # this example also demonstrates a way of passing callbacks from Wren to Nim, 280 | # but this works for any Wren type (eg. classes) 281 | var onTickFn: WrenRef 282 | 283 | proc onTick(callback: WrenRef) = 284 | onTickFn = callback 285 | 286 | wren.foreign("engine"): 287 | [Engine]: 288 | onTick 289 | wren.ready() 290 | 291 | wren.run(code) 292 | 293 | let methodCall0 = wren{"call()"} 294 | wren.call(methodCall0, onTickFn) 295 | ``` 296 | ```js 297 | import "engine" for Engine 298 | Engine.onTick { 299 | System.println("Hello from callback!") 300 | } 301 | ``` 302 | 303 | Note that the Wren VM **is not reentrant**, meaning you cannot call Wren in 304 | a foreign method. 305 | 306 | ### Binding objects 307 | 308 | Binding objects is very similar to procs. All *public* object fields are 309 | exported to Wren. 310 | 311 | If a proc returns an object, the class for that object must be declared *before* 312 | the proc is declared. 313 | 314 | ```nim 315 | type 316 | Foo = object 317 | name*: string 318 | count: int 319 | 320 | proc initFoo(name: string): Foo = 321 | result = Foo() 322 | result.name = name 323 | result.count = 1 324 | 325 | proc more(foo: var Foo) = 326 | inc(foo.count) 327 | 328 | proc count(foo: Foo) = foo.count 329 | 330 | wren.foreign("foo"): 331 | # objects are declared without [] and can be aliased, just like procs 332 | Foo -> Bar: 333 | # the * operator makes the bound proc static 334 | # this is not needed in namespaces, and produces a warning 335 | *initFoo -> new 336 | # procs can be bound as usual 337 | more 338 | # ? binds the proc for use with getter syntax 339 | # (``x.count`` instead of ``x.count()``) 340 | ?count 341 | wren.ready() 342 | ``` 343 | ```js 344 | import "foo" for Bar 345 | 346 | var foo = Bar.new("Thing") 347 | foo.more() 348 | System.print(foo.count) 349 | ``` 350 | 351 | *Concrete* generic types are supported: 352 | 353 | ```nim 354 | import strformat 355 | 356 | type 357 | Vec2[T] = object 358 | x*, y*: float 359 | 360 | proc vec2[T](x, y: T): T = 361 | result = Vec2[T](x: x, y: y) 362 | 363 | proc `+`[T](a, b: Vec2[T]): Vec2[T] = 364 | result = Vec2[T](x: a.x + b.x, y: a.y + b.y) 365 | 366 | proc `$`[T](a: Vec2[T]): string = 367 | result = fmt"[{a.x} {a.y}]" 368 | 369 | wren.foreign("concrete_generic"): 370 | # concrete generic types *must* be aliased 371 | Vec2[float] -> Vec2f: 372 | # you must fill any generic types on procs 373 | # failing to do so will yield in a compile error, which is not caught 374 | # by euwren (yet) 375 | *vec2(float, float) -> new 376 | `+`(Vec2[float], Vec2[float]) 377 | `$`(Vec2[float]) 378 | ``` 379 | ```js 380 | import "concrete_generic" for Vec2f 381 | 382 | var a = Vec2f.new(10, 10) 383 | var b = Vec2f.new(20, 30) 384 | var c = a + b 385 | 386 | System.print(c) // [30 40] 387 | ``` 388 | 389 | ### Binding enums 390 | 391 | ```nim 392 | type 393 | Fruit = enum 394 | fruitApple 395 | fruitBanana 396 | fruitGrape 397 | MenuOpt = enum 398 | optStart 399 | optHelp 400 | optExit 401 | ProgLanguage = enum 402 | langNim 403 | langWren 404 | langC 405 | 406 | wren.foreign("enums"): 407 | # enums are bound by not specifying a body 408 | Fruit 409 | # the conventional prefix can be stripped by using ``-`` 410 | MenuOpt - opt 411 | # enums can also be aliased 412 | ProgLanguage - lang -> Lang 413 | wren.ready() 414 | ``` 415 | The generated class includes all the values of an enum, and additionally, 416 | `low` and `high` for utility purposes. This also means that you should refrain 417 | from naming your enums in `snake_case`, as your names may clash with the 418 | built-in `low` and `high` properties. An option may be added in the future to 419 | automatically convert your enum to `PascalCase`. 420 | 421 | Here's an example of a generated module, based on the above input: 422 | ```js 423 | class Fruit { 424 | static fruitApple { 0 } 425 | static fruitBanana { 1 } 426 | static fruitGrape { 2 } 427 | static low { 0 } 428 | static high { 2 } 429 | } 430 | class MenuOpt { 431 | static Start { 0 } 432 | static Help { 1 } 433 | static Exit { 2 } 434 | static low { 0 } 435 | static high { 2 } 436 | } 437 | class Lang { 438 | static Nim { 0 } 439 | static Wren { 1 } 440 | static C { 2 } 441 | static low { 0 } 442 | static high { 2 } 443 | } 444 | ``` 445 | ```js 446 | import "enums" for Fruit, MenuOpt, Lang 447 | 448 | System.print(Fruit.fruitGrape) // 2 449 | System.print(MenuOpt.Start) // 0 450 | System.print(Lang.Wren) // 1 451 | ``` 452 | 453 | ### Compile-time flags 454 | - `-d:euwrenDumpForeignModule` – dumps the Wren module source code generated by 455 | `foreign()` upon runtime. Useful for debugging code generation. 456 | - `-d:euwrenDumpClasses` – dumps a list of all bound classes, in the order 457 | they're bound in `foreign()`. 458 | 459 | ### Gotchas 460 | 461 | - Three extra macros called `addProcAux`, `addClassAux`, and `genEnumAux` are 462 | exposed in the public API. **Do not use them in your code.** They are used 463 | internally by `foreign()`, and they make the DSL possible by deferring all 464 | binding to the semantic phase. There are lots of implementation details here, 465 | feel free to read the source code if you're interested. 466 | - Currently, euwren uses a fork of Wren that fixes an issue related to slots 467 | in the VM. This fork is not the same as the current stable version of Wren, 468 | but it will be used until [Wren/#712](https://github.com/wren-lang/wren/pull/712) 469 | is merged. 470 | - The generated glue code is assembled at run time, which is inefficient and 471 | possibly slows binding down. This will be fixed in a future release. 472 | 473 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [x] Basics 2 | - [x] Running code 3 | - [x] Obtaining variables from Wren 4 | - [x] Calling Wren from Nim 5 | - [x] `foreign()` 6 | - [x] Procs 7 | - [ ] Argument type-based overloads 8 | - [x] Aborting fibers on exceptions in foreign procs 9 | - [x] Objects 10 | - [x] Automatic public field binding 11 | - [x] Enums 12 | - [x] Symbol aliasing 13 | - [x] Automatic Wren module generation 14 | - [x] Type checking 15 | - [x] Inheritance 16 | - [ ] Generics? 17 | Don't know if it's possible, but it's worth giving a shot. 18 | 19 | -------------------------------------------------------------------------------- /euwren.nimble: -------------------------------------------------------------------------------- 1 | #-- 2 | # Package 3 | #-- 4 | 5 | version = "0.13.3" 6 | author = "liquid600pgm" 7 | description = "High-level Wren wrapper" 8 | license = "MIT" 9 | srcDir = "src" 10 | 11 | #-- 12 | # Dependencies 13 | #-- 14 | 15 | requires "nim >= 1.0.0" 16 | requires "nimterop >= 0.3.6" 17 | -------------------------------------------------------------------------------- /src/euwren.nim: -------------------------------------------------------------------------------- 1 | import macros 2 | import strutils 3 | import tables 4 | 5 | import euwren/private/wren 6 | 7 | #-- 8 | # Definitions 9 | #-- 10 | 11 | type 12 | RawVM* = ptr WrenVM 13 | 14 | MethodSign = tuple[module, class, name: string, isStatic: bool] 15 | ClassSign = tuple[module, name: string] 16 | 17 | WrenType* = enum 18 | wtBool = "bool" 19 | wtNumber = "number" 20 | wtForeign = "foreign" 21 | wtList = "list" 22 | wtNull = "null" 23 | wtString = "string" 24 | wtUnknown = "" 25 | 26 | WrenRef* = ref object 27 | vm: Wren 28 | handle: ptr WrenHandle 29 | WrenValueKind = enum 30 | wvkBool 31 | wvkNumber 32 | wvkString 33 | wvkWrenRef 34 | WrenValue = object 35 | case kind: WrenValueKind 36 | of wvkBool: boolVal: bool 37 | of wvkNumber: numVal: float 38 | of wvkString: strVal: string 39 | of wvkWrenRef: wrenRef: WrenRef 40 | ModuleVar = tuple[module, variable: string] 41 | 42 | Wren* = ref object 43 | ## A Wren virtual machine used for executing code. 44 | handle: RawVM 45 | procWrite: proc (str: string) 46 | procResolveModule: proc (importer, name: string): string 47 | procLoadModule: proc (path: string): string 48 | 49 | methods: Table[MethodSign, WrenForeignMethodFn] 50 | classes: Table[ClassSign, WrenForeignClassMethods] 51 | 52 | typeNames: Table[uint16, string] 53 | parentTypeIds: Table[uint16, set[uint16]] 54 | 55 | compileErrors: seq[WrenError] 56 | rtError: WrenError 57 | WrenErrorKind* = enum 58 | weCompile ## A compilation error (eg. syntax error). 59 | weRuntime ## A runtime error (eg. ``Fiber.abort()``). 60 | WrenError* = object of CatchableError 61 | ## A Wren error. This is raised when an error occurs *inside the VM.* 62 | module*: string 63 | line*: int 64 | message*: string 65 | case kind*: WrenErrorKind 66 | of weCompile: discard 67 | of weRuntime: 68 | stackTrace*: seq[tuple[module: string, line: int, message: string]] 69 | 70 | proc `$`*(vm: Wren): string = 71 | ## Return a string representation of the Wren instance. Keep in mind this 72 | ## doesn't really hold much useful information. 73 | result = "- Wren instance\n" & 74 | "VM: " & $cast[int](vm.handle) & '\n' 75 | 76 | proc raw*(vm: Wren): RawVM = 77 | ## Returns the raw VM from the Wren instance. 78 | vm.handle 79 | 80 | proc raw*(wr: WrenRef): ptr WrenHandle = 81 | ## Returns the raw WrenHandle. 82 | wr.handle 83 | 84 | proc newWren*(): Wren = 85 | ## Creates a new VM. 86 | new(result) do (vm: Wren): 87 | wrenFreeVM(vm.handle) 88 | 89 | var config: WrenConfiguration 90 | wrenInitConfiguration(addr config) 91 | # debugging 92 | config.writeFn = proc (vm: RawVM, text: cstring) {.cdecl.} = 93 | cast[Wren](wrenGetUserData(vm)).procWrite($text) 94 | config.errorFn = proc (vm: RawVM, ty: WrenErrorType, module: cstring, 95 | line: cint, msg: cstring) {.cdecl.} = 96 | var wvm = cast[Wren](wrenGetUserData(vm)) 97 | case ty 98 | of WREN_ERROR_COMPILE: 99 | var err = WrenError( 100 | kind: weCompile, 101 | module: $module, 102 | line: line.int, 103 | message: $msg 104 | ) 105 | wvm.compileErrors.add(err) 106 | of WREN_ERROR_RUNTIME: 107 | var err = WrenError( 108 | kind: weRuntime, 109 | message: $msg 110 | ) 111 | wvm.rtError = err 112 | of WREN_ERROR_STACK_TRACE: 113 | wvm.rtError.stackTrace.add((module: $module, 114 | line: line.int, 115 | message: $msg)) 116 | else: doAssert(false) # unreachable 117 | # modules 118 | config.loadModuleFn = proc (vm: RawVM, name: cstring): cstring {.cdecl.} = 119 | let 120 | source = cast[Wren](wrenGetUserData(vm)).procLoadModule($name) 121 | cssource = alloc0((source.len + 1) * sizeof(char)) 122 | if source.len > 0: 123 | cssource.copyMem(source[0].unsafeAddr, source.len * sizeof(char)) 124 | result = cast[cstring](cssource) 125 | config.resolveModuleFn = proc (vm: RawVM, importer, 126 | name: cstring): cstring {.cdecl.} = 127 | let 128 | source = 129 | cast[Wren](wrenGetUserData(vm)).procResolveModule($importer, $name) 130 | cssource = alloc0((source.len + 1) * sizeof(char)) 131 | if source.len > 0: 132 | cssource.copyMem(source[0].unsafeAddr, source.len * sizeof(char)) 133 | result = cast[cstring](cssource) 134 | else: 135 | result = nil 136 | # FFI 137 | config.bindForeignMethodFn = proc (vm: RawVM, module: cstring, 138 | class: cstring, isStatic: bool, 139 | name: cstring): WrenForeignMethodFn 140 | {.cdecl.} = 141 | var wvm = cast[Wren](wrenGetUserData(vm)) 142 | let sign = ($module, $class, $name, isStatic).MethodSign 143 | if sign in wvm.methods: 144 | result = wvm.methods[sign] 145 | else: 146 | result = nil 147 | config.bindForeignClassFn = proc (vm: ptr WrenVM, module: cstring, 148 | class: cstring): WrenForeignClassMethods 149 | {.cdecl.} = 150 | var wvm = cast[Wren](wrenGetUserData(vm)) 151 | let sign = ($module, $class).ClassSign 152 | if sign in wvm.classes: 153 | result = wvm.classes[sign] 154 | else: 155 | result = WrenForeignClassMethods() 156 | # memory 157 | config.reallocateFn = proc (mem: pointer, newSize: csize): pointer {.cdecl.} = 158 | result = realloc(mem, newSize.Natural) 159 | 160 | result.handle = wrenNewVM(addr config) 161 | wrenSetUserData(result.handle, cast[pointer](result)) 162 | 163 | result.procWrite = proc (str: string) = 164 | stdout.write(str) 165 | result.procLoadModule = proc (path: string): string = 166 | result = "" 167 | result.procResolveModule = proc (importer, name: string): string = 168 | result = name 169 | 170 | result.rtError = WrenError(kind: weRuntime) 171 | 172 | proc onWrite*(vm: Wren, callback: proc (str: string)) = 173 | ## Sets the write callback for the VM. This callback is called when the Wren 174 | ## VM wants to print something out to the console. The default callback simply 175 | ## writes to stdout. 176 | vm.procWrite = callback 177 | 178 | proc onLoadModule*(vm: Wren, callback: proc (path: string): string) = 179 | ## Sets the load module callback for the VM. The callback is called when 180 | ## the VM occurs an ``import`` statement, and doesn't have the given module 181 | ## loaded yet. The callback should then return the source code of the module 182 | ## at ``path``. If the callback returns an empty string, a module that aborts 183 | ## the fiber will be loaded (using ``import`` will throw an error). The 184 | ## default implementation returns an empty string, so override this if you 185 | ## want imports to work. 186 | vm.procLoadModule = callback 187 | 188 | proc onResolveModule*(vm: Wren, 189 | callback: proc (importer, name: string): string) = 190 | ## Sets the resolve module callback for the VM. The callback is called when 191 | ## the VM occurs an ``import`` statement, to resolve what module should 192 | ## actually be loaded. This is usually used to implement relative imports. 193 | ## If the callback returns an empty string, the VM will raise an error saying 194 | ## that the requested module could not be found. The default implementation 195 | ## simply returns ``name`` without any side effects. 196 | vm.procResolveModule = callback 197 | 198 | proc newRef(vm: Wren, handle: ptr WrenHandle): WrenRef = 199 | ## Create a new, Nim GC-managed WrenRef out of a raw WrenHandle pointer. 200 | assert vm != nil 201 | assert handle != nil 202 | new(result) do (wref: WrenRef): 203 | wrenReleaseHandle(wref.vm.handle, wref.handle) 204 | result.vm = vm 205 | result.handle = handle 206 | 207 | #-- 208 | # Low-level APIs 209 | #-- 210 | 211 | # Ultimately, you shouldn't need to use these APIs. They're inherently unsafe, 212 | # and don't provide any guarantees or assertions. In fact, they're only a thin 213 | # wrapper over the underlying Wren embedding API. They're exported only to 214 | # make the high-level API possible. 215 | 216 | # Use with care. 217 | 218 | proc ensureSlots*(vm: RawVM, amount: int) = 219 | wrenEnsureSlots(vm, amount.cint) 220 | 221 | proc slotCount*(vm: RawVM): int = 222 | wrenGetSlotCount(vm) 223 | 224 | macro genericParam(T: typed, index: int): untyped = 225 | ## Get the generic param at position ``index`` from T. 226 | result = T.getTypeInst[1][index.intVal.int] 227 | macro genericParam(T: typed): untyped = 228 | ## Get the actual type behind T. 229 | result = T.getTypeInst[1] 230 | 231 | proc genTypeCheck(vm, ty, slot: NimNode): NimNode 232 | macro checkType(vm, ty: typed, slot: int): untyped = 233 | result = genTypeCheck(vm, ty, slot) 234 | 235 | proc getWrenName(typeSym: NimNode): string 236 | macro wrenName(ty: typed): untyped = 237 | result = newLit(getWrenName(ty)) 238 | 239 | proc getSlotType*(vm: RawVM, slot: int): WrenType = 240 | result = wrenGetSlotType(vm, slot.cint).WrenType 241 | 242 | proc getSlotForeignId*(vm: RawVM, slot: int): uint16 = 243 | result = cast[ptr uint16](wrenGetSlotForeign(vm, slot.cint))[] 244 | 245 | proc getSlotTypeString*(vm: RawVM, slot: int): string = 246 | let ty = vm.getSlotType(slot) 247 | if ty != wtForeign: result = $ty 248 | else: 249 | let wvm = cast[Wren](wrenGetUserData(vm)) 250 | result = wvm.typeNames[vm.getSlotForeignId(slot)] 251 | 252 | proc getSlotForeign*[T](vm: RawVM, slot: int): ptr T = 253 | let raw = cast[ptr UncheckedArray[uint16]](wrenGetSlotForeign(vm, slot.cint)) 254 | result = cast[ptr T](raw[1].unsafeAddr) 255 | 256 | proc getSlot*[T](vm: RawVM, slot: int): T = 257 | when T is bool: 258 | result = wrenGetSlotBool(vm, slot.cint) 259 | elif T is SomeNumber: 260 | result = T(wrenGetSlotDouble(vm, slot.cint)) 261 | elif T is enum: 262 | result = T(wrenGetSlotDouble(vm, slot.cint).int) 263 | elif T is string: 264 | var 265 | len: cint 266 | bytes = wrenGetSlotBytes(vm, slot.cint, addr len) 267 | result = newString(len.Natural) 268 | if len > 0: 269 | copyMem(result[0].unsafeAddr, bytes, len.Natural) 270 | elif T is array | seq: 271 | when T is array: 272 | const 273 | P = 2 274 | Min = ord(genericParam(T, 1).a) 275 | Max = ord(genericParam(T, 1).b) 276 | Len = Max - Min + 1 277 | else: 278 | const 279 | P = 1 280 | Min = 0 281 | let listLen = wrenGetListCount(vm, slot.cint) 282 | when T is seq: 283 | result.setLen(listLen) 284 | else: 285 | if listLen != Len: 286 | vm.abortFiber("got list of length " & $listLen & ", but the expected " & 287 | "length is " & $Len) 288 | return 289 | let listHandle = wrenGetSlotHandle(vm, slot.cint) 290 | for i in 0.. in list, " & 298 | "but expected <" & wrenName(genericParam(T, P)) & ">") 299 | return 300 | wrenReleaseHandle(vm, listHandle) 301 | elif T is WrenRef: 302 | result = cast[Wren](wrenGetUserData(vm)) 303 | .newRef(wrenGetSlotHandle(vm, slot.cint)) 304 | elif T is object | tuple | ref: 305 | result = getSlotForeign[T](vm, slot)[] 306 | else: 307 | {.error: "unsupported type for slot retrieval: " & $T.} 308 | 309 | proc newForeign*(vm: RawVM, slot: int, size: Natural, classSlot = 0): pointer = 310 | result = wrenSetSlotNewForeign(vm, slot.cint, classSlot.cint, size.cuint) 311 | 312 | proc getVariable*(vm: RawVM, slot: int, module, variable: string) = 313 | wrenGetVariable(vm, module, variable, slot.cint) 314 | 315 | var wrenNames {.compileTime.}: Table[uint16, ModuleVar] ## \ 316 | ## Maps unique type IDs to their corresponding variables in bound modules. 317 | 318 | proc getTypeId(typeSym: NimNode): uint16 {.compileTime.} 319 | macro wrenVar(T: typed): untyped = 320 | let id = getTypeId(T) 321 | if id notin wrenNames: 322 | error("type <" & T.repr & "> is unknown to the VM", T) 323 | let v = wrenNames[id] 324 | result = newTree(nnkPar, newLit(v.module), newLit(v.variable)) 325 | 326 | proc genForeignObjectInit(vm, objType, expr, slot: NimNode, 327 | exprIsInit = false): NimNode 328 | macro foreignObjectInit(vm, objType, expr: typed, slot: int, 329 | exprIsInit = false): untyped = 330 | result = genForeignObjectInit(vm, objType, expr, slot, 331 | exprIsInit.boolVal) 332 | 333 | proc setSlot*[T](vm: RawVM, slot: int, val: T) = 334 | when T is bool: 335 | wrenSetSlotBool(vm, slot.cint, val) 336 | elif T is SomeNumber: 337 | wrenSetSlotDouble(vm, slot.cint, val.cdouble) 338 | elif T is enum: 339 | wrenSetSlotDouble(vm, slot.cint, ord(val).cdouble) 340 | elif T is string: 341 | wrenSetSlotBytes(vm, slot.cint, val, val.len.cuint) 342 | elif T is array | seq: 343 | const P = 344 | when T is array: 2 345 | else: 1 346 | wrenEnsureSlots(vm, cint(slot + 1)) 347 | wrenSetSlotNewList(vm, slot.cint) 348 | for x in val: 349 | setSlot[genericParam(T, P)](vm, slot + 1, x) 350 | wrenInsertInList(vm, slot.cint, -1, cint(slot + 1)) 351 | elif T is WrenRef: 352 | wrenSetSlotHandle(vm, slot.cint, val.handle) 353 | elif T is object | tuple | ref: 354 | let varInfo = wrenVar(genericParam(T)) 355 | vm.getVariable(slot, varInfo[0], varInfo[1]) 356 | foreignObjectInit(vm, genericParam(T), val, slot) 357 | else: 358 | {.error: "unsupported type for slot assignment: " & $T.} 359 | 360 | proc abortFiber*(vm: RawVM, message: string) = 361 | vm.setSlot[:string](0, message) 362 | wrenAbortFiber(vm, 0) 363 | 364 | proc checkParent*(vm: RawVM, base, compare: uint16): bool = 365 | ## Check if the ``compare`` type is one of ``base``'s parent types. 366 | ## Used internally for type checking with inheritance. 367 | let wvm = cast[Wren](wrenGetUserData(vm)) 368 | result = base in wvm.parentTypeIds[compare] 369 | 370 | proc addTypeInfo*(vm: Wren, id: uint16, name: string, parents: set[uint16]) = 371 | ## This is an implementation detail used internally by the wrapper. 372 | ## You should not use this in your code. 373 | vm.typeNames[id] = name 374 | vm.parentTypeIds[id] = parents 375 | 376 | proc addProc*(vm: Wren, module, class, signature: string, isStatic: bool, 377 | impl: WrenForeignMethodFn) = 378 | vm.methods[(module, class, signature, isStatic)] = impl 379 | 380 | proc addClass*(vm: Wren, module, name: string, 381 | destroy: WrenFinalizerFn = nil) = 382 | vm.classes[(module, name)] = WrenForeignClassMethods( 383 | allocate: nil, 384 | finalize: destroy 385 | ) 386 | 387 | #-- 388 | # End user API - basics 389 | #-- 390 | 391 | proc getError(vm: Wren, interpretResult: WrenInterpretResult): ref WrenError = 392 | case interpretResult 393 | of WREN_RESULT_SUCCESS: discard 394 | of WREN_RESULT_COMPILE_ERROR: 395 | var err = new(WrenError) 396 | err.msg = "compile error" 397 | for e in vm.compileErrors: 398 | err.msg &= '\n' & e.module & '(' & $e.line & "): " & e.message 399 | result = err 400 | of WREN_RESULT_RUNTIME_ERROR: 401 | var err = new(WrenError) 402 | err.msg = vm.rtError.message & "\nwren stack trace:" 403 | for t in vm.rtError.stackTrace: 404 | err.msg &= "\n at " & t.module & '(' & $t.line & ')' 405 | result = err 406 | else: doAssert(false) # unreachable 407 | 408 | proc checkRuntimeError(vm: Wren, interpretResult: WrenInterpretResult) = 409 | if interpretResult != WREN_RESULT_SUCCESS: 410 | raise vm.getError(interpretResult) 411 | 412 | proc module*(vm: Wren, name, src: string) = 413 | ## Runs the provided source code inside of the specified module. 414 | vm.checkRuntimeError(wrenInterpret(vm.handle, name, src)) 415 | 416 | proc run*(vm: Wren, src: string) = 417 | ## Runs the provided source code inside of a module named "main". This should 418 | ## be used for the entry point of your program. Use ``module`` if you want to 419 | ## modify the module name (used in error messages and imports). 420 | vm.module("main", src) 421 | 422 | proc `[]`*(vm: Wren, module, variable: string, T: typedesc = WrenRef): T = 423 | ## Retrieves a variable from the Wren VM. This works both for primitives and 424 | ## Wren objects (pass ``WrenRef`` to ``T`` to retrieve a Wren object). 425 | ## If the variable type does not match ``T``, an error will be thrown nagging 426 | ## the application user. 427 | wrenEnsureSlots(vm.handle, 1) 428 | wrenGetVariable(vm.handle, module, variable, 0) 429 | if not checkType(vm.handle, genericParam(T), 0): 430 | raise newException(WrenError, 431 | "in wren module '" & module & "': variable '" & 432 | variable & "' has type <" & 433 | vm.handle.getSlotTypeString(0) & ">, but expected <" & 434 | wrenName(genericParam(T)) & ">") 435 | result = getSlot[T](vm.handle, 0) 436 | 437 | proc `{}`*(vm: Wren, signature: string): WrenRef = 438 | ## Creates a 'call handle' to the method denoted by ``methodSignature``. 439 | result = vm.newRef(wrenMakeCallHandle(vm.handle, signature)) 440 | 441 | proc toWrenValue*(val: bool): WrenValue = 442 | WrenValue(kind: wvkBool, boolVal: val) 443 | proc toWrenValue*(val: int): WrenValue = 444 | WrenValue(kind: wvkNumber, numVal: val.float) 445 | proc toWrenValue*(val: float): WrenValue = 446 | WrenValue(kind: wvkNumber, numVal: val) 447 | proc toWrenValue*(val: string): WrenValue = 448 | WrenValue(kind: wvkString, strVal: val) 449 | proc toWrenValue*(val: WrenRef): WrenValue = 450 | WrenValue(kind: wvkWrenRef, wrenRef: val) 451 | 452 | proc call*[T](theMethod: WrenRef, 453 | receiver: WrenRef, args: varargs[WrenValue, toWrenValue]): T = 454 | ## Calls the given method with the given arguments. The first argument must 455 | ## always be present, and is the receiver of the method. The rest of the 456 | ## arguments is optional. The generic parameter decides on the return type of 457 | ## the method (which can be void). 458 | ## 459 | ## **Design note:** The ``receiver`` param only accepts ``WrenRef``, because 460 | ## it's pretty much never useful to call a method on a primitive type, since 461 | ## the native implementation is always faster. 462 | let vm = theMethod.vm 463 | wrenEnsureSlots(vm.handle, cint(1 + args.len)) 464 | vm.handle.setSlot[:WrenRef](0, receiver) 465 | for i, arg in args: 466 | case arg.kind 467 | of wvkBool: vm.handle.setSlot[:bool](i + 1, arg.boolVal) 468 | of wvkNumber: vm.handle.setSlot[:float](i + 1, arg.numVal) 469 | of wvkString: vm.handle.setSlot[:string](i + 1, arg.strVal) 470 | of wvkWrenRef: vm.handle.setSlot[:WrenRef](i + 1, arg.wrenRef) 471 | vm.checkRuntimeError(wrenCall(vm.handle, theMethod.handle)) 472 | when T isnot void: 473 | result = vm.handle.getSlot[:T](0) 474 | 475 | #-- 476 | # End user API - foreign() 477 | #-- 478 | 479 | # leaving this here for my own debugging convenience. 480 | proc `$`(n: NimNode): string {.used.} = n.repr 481 | 482 | proc getParamList(formalParams: NimNode): seq[NimNode] = 483 | ## Flattens an nnkFormalParams into a C-like list of argument types, 484 | ## eg. ``x, y: int`` becomes ``@[int, int]``. 485 | for identDefs in formalParams[1..^1]: 486 | let ty = 487 | if identDefs[^2].kind != nnkEmpty: identDefs[^2] 488 | else: 489 | if identDefs[^1].kind != nnkIdent: identDefs[^1].getType 490 | else: newEmptyNode() 491 | for i in 0.. (of kind " & $typeSym.typeKind & 605 | ") is not a supported foreign type", typeSym) 606 | let hash = typeSym.typeHash 607 | if hash notin typeIds: 608 | let 609 | id = typeIds.len.uint16 610 | parent = typeSym.getParent 611 | typeIds[hash] = id 612 | typeNames[id] = typeSym.repr 613 | if parent == nil: 614 | parentTypeIds[id] = {} 615 | else: 616 | let parentId = getTypeId(parent) 617 | parentTypeIds[id] = parentTypeIds[parentId] + {parentId} 618 | result = typeIds[hash] 619 | 620 | proc isRef(class: NimNode): bool = 621 | ## Checks if the given type symbol represents a ref type. 622 | result = class.flattenType.typeKind == ntyRef 623 | 624 | proc newCast(T, val: NimNode): NimNode = 625 | ## Create a new nnkCast node, which casts ``val`` to ``T``. 626 | newTree(nnkCast, T, val) 627 | 628 | type 629 | Empty = object ## A dummy object to represent an nnkEmpty. 630 | TypePair = array[2, NimNode] 631 | 632 | proc getType(pair: TypePair): NimNode = 633 | if pair[0] != bindSym"Empty": result = pair[0] 634 | else: result = pair[1].getTypeInst 635 | 636 | proc getSlotGetters(params: openarray[TypePair], 637 | isStatic: bool): seq[NimNode] = 638 | ## Get a list of getSlot() calls which extract the given parameters from 639 | ## the VM. 640 | for i, pair in params: 641 | let 642 | slot = newLit(i + ord(isStatic)) 643 | paramType = pair.getType 644 | getter = 645 | if paramType.typeKind in {ntyObject, ntyVar} or paramType.isRef: 646 | if paramType.typeKind == ntyVar: 647 | newCast(paramType, 648 | newCall(newTree(nnkBracketExpr, ident"getSlotForeign", 649 | paramType[0]), 650 | ident"vm", slot)) 651 | else: 652 | newTree(nnkBracketExpr, 653 | newCall(newTree(nnkBracketExpr, ident"getSlotForeign", 654 | paramType), 655 | ident"vm", slot)) 656 | else: 657 | newCall(newTree(nnkBracketExpr, ident"getSlot", paramType), 658 | ident"vm", slot) 659 | result.add(getter) 660 | 661 | proc genTypeCheck(vm, ty, slot: NimNode): NimNode = 662 | # type kind sets 663 | const 664 | Nums = {ntyInt..ntyUint64, ntyEnum} 665 | Lists = {ntyArray, ntySequence} 666 | Foreign = {ntyObject, ntyRef, ntyTuple, ntyGenericBody} 667 | let ty = ty.flattenType 668 | # generate the check 669 | let 670 | wrenType = 671 | if ty.typeKind == ntyBool: wtBool 672 | elif ty.typeKind in Nums: wtNumber 673 | elif ty.typeKind == ntyString: wtString 674 | elif ty == bindSym"WrenRef": wtUnknown 675 | elif ty.kind == nnkBracketExpr: 676 | let subTy = ty[0].flattenType 677 | if subTy.typeKind in Lists: wtList 678 | elif subTy.typeKind in Foreign: wtForeign 679 | else: 680 | error("unsupported generic type kind: " & $ty.typeKind & ' ' & 681 | "for <" & ty.repr & ">", ty) 682 | wtUnknown 683 | elif ty.typeKind in Foreign: wtForeign 684 | else: 685 | error("unsupported type kind: " & $ty.typeKind & 686 | " for <" & ty.repr & ">", ty) 687 | wtUnknown 688 | comparison = newTree(nnkInfix, ident"==", 689 | newCall("getSlotType", vm, slot), 690 | newLit(wrenType)) 691 | result = comparison 692 | if wrenType == wtForeign: 693 | let 694 | typeId = getTypeId(ty) 695 | typeIdLit = newLit(typeId) 696 | slotId = newCall("getSlotForeignId", vm, slot) 697 | idCheck = newTree(nnkInfix, ident"==", slotId, typeIdLit) 698 | parentCheck = newCall(ident"checkParent", vm, typeIdLit, slotId) 699 | result = newTree(nnkInfix, ident"and", result, 700 | newPar(newTree(nnkInfix, ident"or", idCheck, parentCheck))) 701 | 702 | proc genTypeChecks(vm: NimNode, isStatic: bool, 703 | typePairs: varargs[TypePair]): NimNode = 704 | ## Generate a type check condition. This looks at all the params and assembles 705 | ## a big chain of conditions which check the type. 706 | ## This is a much better way of checking types compared to the 0.1.0 707 | ## ``checkTypes``, which simply looped through an array of ``WrenTypeData`` 708 | ## structs and compared them. The current, macro-based version, has much lower 709 | ## runtime overhead, because it's just a simple chain of conditions. 710 | 711 | # there isn't any work to be done if the proc doesn't accept params 712 | if typePairs.len == 0 or typePairs.len == ord(not isStatic): 713 | return newLit(true) 714 | 715 | # place the actual types into a seq 716 | var types: seq[NimNode] 717 | for pair in typePairs: 718 | types.add(pair.getType) 719 | # if the first param is var, ignore that 720 | if types[0].kind == nnkVarTy: 721 | types[0] = types[0][0] 722 | 723 | # generate a list of checks 724 | var checks: seq[NimNode] 725 | for i, ty in types[ord(not isStatic)..^1]: 726 | let slot = i + 1 727 | checks.add(genTypeCheck(vm, ty, newLit(slot))) 728 | # fold the list of checks to an nnkInfix node 729 | result = checks[^1] 730 | for i in countdown(checks.len - 2, 0): 731 | result = newTree(nnkInfix, ident"and", checks[i], result) 732 | 733 | proc getWrenName(typeSym: NimNode): string = 734 | ## Get the Wren name for the corresponding type. This aliases number types 735 | ## to ``number``, ``WrenRef`` to ``object``, and any Wren-bound types to 736 | ## their names in the Wren VM. 737 | let typeSym = typeSym.flattenType 738 | if typeSym.typeKind in {ntyInt..ntyUint64}: result = "number" 739 | elif typeSym == bindSym"WrenRef": result = "object" 740 | elif typeSym.typeKind in {ntyObject, ntyRef} and 741 | getTypeId(typeSym) in wrenNames: 742 | let id = getTypeId(typeSym) 743 | result = wrenNames[id].variable 744 | else: result = typeSym.repr 745 | 746 | proc genTypeError(theProc: NimNode, wrenName: string, arity: int, 747 | overloads: varargs[NimNode]): NimNode = 748 | ## Generate a Nim-like type mismatch error. 749 | result = newStmtList() 750 | let 751 | errVar = newVarStmt(ident"err", 752 | newLit("type mismatch: got <")) 753 | fiberAbort = newCall("abortFiber", ident"vm", ident"err") 754 | result.add([errVar]) 755 | for i in 1..arity: 756 | result.add(newCall("add", ident"err", 757 | newCall("getSlotTypeString", ident"vm", newLit(i)))) 758 | if i != arity: 759 | result.add(newCall("add", ident"err", newLit", ")) 760 | var expectedStr = "" 761 | for overload in overloads: 762 | let 763 | impl = overload.getImpl 764 | params = impl[3] 765 | expectedStr.add("\n " & wrenName & '(') 766 | for i, defs in params[1..^1]: 767 | for j, def in defs[0..^3]: 768 | expectedStr.add(def.repr) 769 | if j < defs.len - 3: 770 | expectedStr.add(", ") 771 | expectedStr.add(": " & getWrenName(defs[^2])) 772 | if i < params.len - 2: 773 | expectedStr.add(", ") 774 | expectedStr.add(')') 775 | if params[0].kind != nnkEmpty: 776 | expectedStr.add(": " & getWrenName(params[0])) 777 | result.add(newCall("add", ident"err", 778 | newLit(">\nbut expected one of:" & expectedStr))) 779 | result.add(fiberAbort) 780 | 781 | proc genForeignObjectInit(vm, objType, expr, slot: NimNode, 782 | exprIsInit = false): NimNode = 783 | ## Generate a statement list with the initialization procedure for a new 784 | ## foreign object. ``expr`` is the expression that needs to be called to 785 | ## initialize the given ``objType``. ``slot`` is the VM slot where the new 786 | ## foreign object should be stored. It is also the slot where the foreign 787 | ## class must be stored. If ``exprIsInit`` is true, ``expr`` will be treated 788 | ## as an initializer instead of a constructor. 789 | result = newStmtList() 790 | # create the foreign object 791 | let 792 | size = newTree(nnkInfix, ident"+", 793 | newCall("sizeof", ident"uint16"), 794 | newCall("sizeof", objType)) 795 | dataSym = genSym(nskVar, "data") 796 | newForeignCall = newCall("newForeign", vm, slot, size, slot) 797 | u16array = newTree(nnkPtrTy, 798 | newTree(nnkBracketExpr, 799 | ident"UncheckedArray", ident"uint16")) 800 | result.add(newVarStmt(dataSym, newCast(u16array, newForeignCall))) 801 | # assign the type ID 802 | let typeId = getTypeId(objType) 803 | result.add(newTree(nnkAsgn, 804 | newTree(nnkBracketExpr, dataSym, newLit(0)), 805 | newLit(typeId))) 806 | # assign the data 807 | let 808 | objSym = genSym(nskVar, "objectData") 809 | ptrObjType = newTree(nnkPtrTy, objType) 810 | objAddr = newTree(nnkAddr, newTree(nnkBracketExpr, dataSym, newLit(1))) 811 | obj = newTree(nnkDerefExpr, objSym) 812 | result.add(newVarStmt(objSym, newCast(ptrObjType, objAddr))) 813 | if exprIsInit: 814 | let 815 | initSym = genSym(nskVar, "init") 816 | initVar = newTree(nnkVarSection, 817 | newTree(nnkIdentDefs, initSym, objType, newEmptyNode())) 818 | result.add(initVar) 819 | var initExpr = expr 820 | initExpr[0] = initSym 821 | result.add(newTree(nnkAsgn, obj, initExpr)) 822 | else: 823 | result.add(newTree(nnkAsgn, obj, expr)) 824 | if objType.isRef: 825 | result.add(newCall("GC_ref", obj)) 826 | 827 | proc genForeignErrorCheck(expr: NimNode): NimNode = 828 | ## Wraps ``expr`` in a try…except statement, which, in case of error, aborts 829 | ## the current fiber with the caught exception's error message. 830 | ## If in a debug build, the message will also contain the stack traceback. 831 | result = newNimNode(nnkTryStmt) 832 | result.add(newStmtList(expr)) 833 | var 834 | branch = newNimNode(nnkExceptBranch) 835 | branchStmts = newStmtList() 836 | let 837 | errSym = genSym(nskLet, "error") 838 | msgSym = genSym(nskVar, "errorMessage") 839 | branchStmts.add(newLetStmt(errSym, newCall("getCurrentException"))) 840 | branchStmts.add(newVarStmt(msgSym, newTree(nnkDotExpr, errSym, ident"msg"))) 841 | branchStmts.add(newCall("add", msgSym, newLit(" ["))) 842 | branchStmts.add(newCall("add", msgSym, 843 | newTree(nnkDotExpr, errSym, ident"name"))) 844 | branchStmts.add(newCall("add", msgSym, newLit(']'))) 845 | when compileOption("stacktrace"): 846 | branchStmts.add(newCall("add", msgSym, newLit("\nnim stack trace:\n"))) 847 | branchStmts.add(newCall("add", msgSym, newCall("getStackTrace", errSym))) 848 | branchStmts.add(newCall("abortFiber", ident"vm", msgSym)) 849 | branch.add(branchStmts) 850 | result.add(branch) 851 | 852 | proc orEmpty(node: NimNode): NimNode = 853 | result = 854 | if node.kind == nnkEmpty: bindSym"Empty" 855 | else: node 856 | 857 | proc getGenericParams(theProc: NimNode, 858 | params: openArray[TypePair]): Table[string, NimNode] = 859 | let 860 | procImpl = theProc.getImpl 861 | procParams = getParamList(procImpl[3]) 862 | for i, param in procParams: 863 | if param.repr notin result: 864 | let userParam = params[i].getType 865 | result[param.repr] = userParam 866 | 867 | proc resolveGenericParams(expr: NimNode, 868 | table: Table[string, NimNode]): NimNode = 869 | if expr.flattenType.typeKind == ntyGenericParam: 870 | result = table[expr.repr] 871 | elif expr.typeKind == ntyGenericInvocation: 872 | result = expr 873 | if expr.repr in table: 874 | result = table[expr.repr] 875 | else: 876 | for i, sub in result[1..^1]: 877 | result[i + 1] = resolveGenericParams(sub, table) 878 | else: 879 | result = expr 880 | 881 | proc genProcGlue(theProc: NimNode, wrenName: string, 882 | isStatic, isGetter: bool, 883 | params: openArray[TypePair]): NimNode = 884 | ## Generate a glue procedure with type checks and VM slot conversions. 885 | 886 | # get some metadata about the proc 887 | let 888 | procImpl = theProc.getImpl 889 | genericParamTable = getGenericParams(theProc, params) 890 | procRetType = resolveGenericParams(procImpl[3][0], genericParamTable) 891 | # create a new anonymous proc; this is our resulting glue proc 892 | result = newProc(params = [newEmptyNode(), 893 | newIdentDefs(ident"vm", ident"RawVM")]) 894 | result.addPragma(ident"cdecl") 895 | var body = newStmtList() 896 | # generate the call 897 | let 898 | call = newCall(theProc, getSlotGetters(params, isStatic)) 899 | callWithReturn = 900 | # no return type 901 | if procRetType.kind == nnkEmpty or eqIdent(procRetType, "void"): call 902 | # some return type 903 | else: newCall(newTree(nnkBracketExpr, ident"setSlot", procRetType), 904 | ident"vm", newLit(0), call) 905 | callWithTry = genForeignErrorCheck(callWithReturn) 906 | # generate type check 907 | let typeCheck = genTypeChecks(ident"vm", isStatic, params) 908 | body.add(newIfStmt((cond: typeCheck, body: callWithTry)) 909 | .add(newTree(nnkElse, genTypeError(theProc, wrenName, 910 | params.len, theProc)))) 911 | result.body = body 912 | 913 | proc genSignature(theProc: NimNode, wrenName: string, 914 | isStatic, isGetter: bool, namedParams = false): string = 915 | ## Generate a Wren signature for the given proc and its properties. 916 | ## If ``namedParams`` is true, a 'nice' signature will be generated with 917 | ## parameter names embedded into it. 918 | 919 | var 920 | param = ord(not isStatic) 921 | paramNames = getParamNames(theProc.getImpl[3]) 922 | arity = paramNames.len 923 | proc params(n: int): string = 924 | ## Generate a string of params like _,_,_,_ 925 | if namedParams: 926 | for i in 1..n: 927 | result.add(paramNames[param]) 928 | if i != n: 929 | result.add(',') 930 | inc(param) 931 | else: 932 | for i in 1..n: 933 | result.add('_') 934 | if i != n: 935 | result.add(',') 936 | 937 | var name = wrenName 938 | name.removePrefix('`') 939 | name.removeSuffix('`') 940 | if not isGetter: 941 | let arity = arity - ord(not isStatic) 942 | if name == "[]": 943 | result = '[' & arity.params & ']' 944 | elif name == "[]=": 945 | result = '[' & (arity - 1).params & "]=(" & params(1) & ")" 946 | else: 947 | result = name & '(' & arity.params & ')' 948 | else: 949 | result = name 950 | 951 | macro genProcAux(vm: Wren, module: string, className: string, 952 | theProc: typed, wrenName: static string, 953 | isStatic, isGetter: static bool, 954 | params: varargs[typed]): untyped = 955 | ## Second step of binding a proc, generates code which binds it to the 956 | ## provided Wren instance. 957 | 958 | # unpack ``params`` into the correct type 959 | var paramList: seq[array[2, NimNode]] 960 | for i in countup(0, params.len - 1, 2): 961 | paramList.add([params[i], params[i + 1]]) 962 | # call ``addProc`` 963 | let 964 | classLit = className 965 | nameLit = newLit(genSignature(theProc, wrenName, isStatic, isGetter)) 966 | result = newStmtList() 967 | result.add(newCall("addProc", vm, module, 968 | classLit, nameLit, newLit(isStatic), 969 | genProcGlue(theProc, wrenName, isStatic, isGetter, 970 | paramList))) 971 | var wrenDecl = "foreign " 972 | if isStatic: 973 | wrenDecl.add("static ") 974 | wrenDecl.add(genSignature(theProc, wrenName, isStatic, isGetter, 975 | namedParams = true)) 976 | wrenDecl.add('\n') 977 | result.add(newCall("add", ident"classMethods", newLit(wrenDecl))) 978 | 979 | proc resolveOverload(procSym: NimNode, overloaded: bool, 980 | params: varargs[NimNode]): NimNode = 981 | ## Resolve the overload of ``procSym``. If ``overloaded`` is true, overload 982 | ## parameters were provided during binding, and ``params`` are the desired 983 | ## overload's parameters. 984 | result = procSym 985 | if procSym == nil: return 986 | if procSym.kind != nnkSym: 987 | if not overloaded: 988 | error("multiple overloads available; " & 989 | "provide the correct overload's parameters", procSym) 990 | result = getOverload(procSym, params) 991 | 992 | macro addProcAux(vm: Wren, module: string, className: string, 993 | procSym: typed, wrenName: string, 994 | overloaded: static bool, isStatic, isGetter: bool, 995 | params: varargs[typed]): untyped = 996 | ## First step of binding a proc, resolves the overload using the given params 997 | ## and defers the binding to ``genProcAux``. This is a workaround for 998 | ## Nim/#12942, to make the bound procedure's parameters ``typed``. 999 | 1000 | # as a workaround for Nim/#12831, unwrap the procSym if it's 1001 | # in an nnkTypeOfExpr 1002 | var procSym = procSym 1003 | if procSym.kind == nnkTypeOfExpr: 1004 | procSym = procSym[0] 1005 | # find the correct overload of the procedure, if applicable 1006 | let 1007 | theProc = resolveOverload(procSym, overloaded, params) 1008 | procImpl = theProc.getImpl 1009 | # defer to genProcAux 1010 | result = newCall(bindSym"genProcAux", vm, module, className, theProc, 1011 | wrenName, isStatic, isGetter) 1012 | # retrieve [type, defaultValue] pairs from the proc's params 1013 | var index = 0 1014 | for defs in procImpl[3][1..^1]: 1015 | var ty = defs[^2].orEmpty 1016 | # resolve generic types 1017 | if ty.typeKind in {ntyGenericParam, ntyGenericInvocation}: 1018 | if index < params.len: 1019 | ty = params[index] 1020 | else: 1021 | error("unresolved generic param; provide all parameters' types", 1022 | procSym) 1023 | let default = defs[^1].orEmpty 1024 | for _ in 0..": 1201 | decl[2].expectKind({nnkIdent, nnkStrLit, nnkAccQuoted}) 1202 | var alias = decl[2] 1203 | if alias.kind == nnkAccQuoted: 1204 | alias = alias[0] 1205 | if alias.kind == nnkStrLit: 1206 | if not alias.strVal.isWrenIdent: 1207 | error("not a valid Wren identifier", alias) 1208 | result = (nim: decl[1], wren: alias.strVal) 1209 | elif decl.kind == nnkPrefix: 1210 | if decl[1].kind == nnkCall: 1211 | result = (nim: decl, wren: decl[1][0].repr) 1212 | elif decl[1].kind in {nnkIdent, nnkAccQuoted}: 1213 | result = (nim: decl, wren: decl[1].repr) 1214 | else: 1215 | error("invalid binding", decl) 1216 | elif decl.kind == nnkCall: 1217 | result = (nim: decl, wren: decl[0].repr) 1218 | elif decl.kind in {nnkIdent, nnkAccQuoted}: 1219 | result = (nim: decl, wren: decl.repr) 1220 | else: 1221 | error("invalid binding", decl) 1222 | 1223 | proc getClassAlias(decl: NimNode): tuple[class, procs: NimNode, 1224 | isNamespace: bool, wren: string] = 1225 | ## A version of ``getAlias`` for class declarations. Uses ``getAlias`` 1226 | ## internally, so the syntax stays consistent. 1227 | if decl.kind == nnkInfix: 1228 | let (nim, wren) = getAlias(decl) 1229 | result = (class: nim, procs: decl[3], isNamespace: false, wren: wren) 1230 | elif decl.kind == nnkCall: 1231 | result = (class: decl[0], procs: decl[1], isNamespace: false, 1232 | wren: decl[0].repr) 1233 | if result.class.kind == nnkBracket: 1234 | result.class = result.class[0] 1235 | result.isNamespace = true 1236 | result.wren = result.class.repr 1237 | else: 1238 | error("invalid binding", decl) 1239 | 1240 | proc genClassBinding(vm, module, decl: NimNode): NimNode = 1241 | ## Generate a set of calls that binds a set of procedures, and optionally, a 1242 | ## Nim object, from the AST provided in ``decl``. This is part of the 1243 | ## ``foreign()`` DSL. 1244 | var stmts = newStmtList() 1245 | stmts.add(newVarStmt(ident"classMethods", newLit(""))) 1246 | let (class, procs, isNamespace, wrenClass) = getClassAlias(decl) 1247 | var classDecl = "class " & wrenClass & "{\n" 1248 | if not isNamespace: 1249 | classDecl = "foreign " & classDecl 1250 | stmts.add(newCall(bindSym"saveWrenName", class, module, newLit(wrenClass))) 1251 | var bindFields = true 1252 | for p in procs: 1253 | if p.kind == nnkDiscardStmt: discard 1254 | elif p.kind == nnkPragma: 1255 | p[0].expectKind(nnkIdent) 1256 | case p[0].strVal 1257 | of "noFields": bindFields = false 1258 | elif p.kind in {nnkStrLit..nnkTripleStrLit}: 1259 | stmts.add(newCall("add", ident"classMethods", p)) 1260 | else: 1261 | let (nim, wren) = getAlias(p) 1262 | var 1263 | theProc = nim 1264 | isStatic = isNamespace 1265 | isGetter = false 1266 | if nim.kind == nnkPrefix: 1267 | case nim[0].strVal 1268 | of "*": 1269 | isStatic = true 1270 | if isNamespace: 1271 | warning("explicit static marker in namespace; " & 1272 | "did you mean to create an object?", nim[0]) 1273 | of "?": isGetter = true 1274 | of "*?", "?*": 1275 | isStatic = true 1276 | isGetter = true 1277 | theProc = nim[1] 1278 | if nim.kind in {nnkPrefix, nnkCall} and nim[^1].kind == nnkDo: 1279 | var procDef = newNimNode(nnkProcDef) 1280 | theProc = genSym(nskProc, wren) 1281 | nim[^1].copyChildrenTo(procDef) 1282 | procDef[0] = theProc 1283 | stmts.add(procDef) 1284 | stmts.add(getAddProcAuxCall(vm, module, class, wrenClass, theProc, wren, 1285 | isStatic, isGetter)) 1286 | if not isNamespace: 1287 | stmts.add(newCall(bindSym"addClassAux", vm, module, class, 1288 | newLit(wrenClass), newLit(bindFields))) 1289 | stmts.add(newCall("add", ident"modSrc", newLit(classDecl))) 1290 | stmts.add(newCall("add", ident"modSrc", ident"classMethods")) 1291 | stmts.add(newCall("add", ident"modSrc", newLit("}\n"))) 1292 | result = newBlockStmt(stmts) 1293 | 1294 | macro genEnumAux(theEnum: typed, wrenName, prefix: string): untyped = 1295 | ## Generates the code required to bind to a Wren VM. 1296 | result = newStmtList() 1297 | let 1298 | impl = theEnum.getImpl[2] 1299 | classDecl = "class " & wrenName.strVal & "{\n" 1300 | var 1301 | i = -1 1302 | enumFields = "" 1303 | for field in impl[1..^1]: 1304 | var name = "" 1305 | if field.kind == nnkIdent: 1306 | name = field.repr 1307 | inc(i) 1308 | elif field.kind == nnkEnumFieldDef: 1309 | name = field[0].repr 1310 | if field[1].kind == nnkIntLit: 1311 | i = field[1].intVal.int 1312 | elif field[1].kind == nnkTupleConstr: 1313 | i = field[1][0].intVal.int 1314 | else: 1315 | inc(i) 1316 | name.removePrefix(prefix.strVal) 1317 | enumFields.add("static " & name & "{" & $i & "}\n") 1318 | result.add(newCall("add", ident"modSrc", newLit(classDecl))) 1319 | result.add(newCall("add", ident"modSrc", newLit(enumFields))) 1320 | 1321 | template propertyField(property: string) = 1322 | result.add(newCall("add", ident"modSrc", 1323 | newLit("static " & property & "{"))) 1324 | result.add(newCall("add", ident"modSrc", 1325 | newTree(nnkPrefix, ident"$", 1326 | newCall("ord", newCall(property, theEnum))))) 1327 | result.add(newCall("add", ident"modSrc", newLit("}\n"))) 1328 | propertyField("low") 1329 | propertyField("high") 1330 | 1331 | result.add(newCall("add", ident"modSrc", newLit("}\n"))) 1332 | 1333 | proc getGenEnumAuxCall(theEnum: NimNode, wrenName, prefix: string): NimNode = 1334 | ## Create a call to ``getEnumAux``, binding the given enum, with the given 1335 | ## ``wrenName``, and optionally a ``prefix`` to be stripped from the enum. If 1336 | ## ``prefix`` is ``""``, nothing will be stripped. 1337 | result = newCall(bindSym"genEnumAux", theEnum, newLit(wrenName), 1338 | newLit(prefix)) 1339 | 1340 | proc genEnumBinding(decl: NimNode): NimNode = 1341 | ## Generates an enum binding from the AST of ``decl``. This is part of the 1342 | ## ``foreign()`` DSL. 1343 | if decl.kind == nnkIdent: 1344 | result = getGenEnumAuxCall(decl, decl.repr, "") 1345 | elif decl.kind == nnkInfix: 1346 | case decl[0].strVal 1347 | of "-": 1348 | decl[1].expectKind(nnkIdent) 1349 | decl[2].expectKind(nnkIdent) 1350 | result = getGenEnumAuxCall(decl[1], decl[1].repr, decl[2].strVal) 1351 | of "->": 1352 | let (nim, wren) = getAlias(decl) 1353 | if nim.kind == nnkIdent: 1354 | result = getGenEnumAuxCall(nim, wren, "") 1355 | elif nim.kind == nnkInfix: 1356 | nim[1].expectKind(nnkIdent) 1357 | nim[2].expectKind(nnkIdent) 1358 | result = getGenEnumAuxCall(nim[1], wren, nim[2].strVal) 1359 | else: 1360 | error("invalid enum declaration", nim) 1361 | else: error("invalid enum declaration operator", decl[0]) 1362 | 1363 | macro foreign*(vm: Wren, module: string, body: untyped): untyped = 1364 | ## Bind foreign things into the Wren VM. Refer to the README for details on 1365 | ## how to use this. 1366 | body.expectKind(nnkStmtList) 1367 | var 1368 | # we begin with an empty module 1369 | # this module is later used in a ``module()`` call 1370 | stmts = newTree(nnkStmtList, newVarStmt(ident"modSrc", newLit(""))) 1371 | for decl in body: 1372 | case decl.kind 1373 | of nnkCall: 1374 | # class bindings 1375 | stmts.add(genClassBinding(vm, module, decl)) 1376 | of nnkIdent, nnkInfix: 1377 | # enums or aliased objects 1378 | if decl.kind == nnkInfix and decl[^1].kind == nnkStmtList: 1379 | stmts.add(genClassBinding(vm, module, decl)) 1380 | else: 1381 | stmts.add(genEnumBinding(decl)) 1382 | of nnkStrLit..nnkTripleStrLit: 1383 | # module code injections 1384 | stmts.add(newCall("add", ident"modSrc", decl)) 1385 | stmts.add(newCall("add", ident"modSrc", newLit('\n'))) 1386 | of nnkDiscardStmt: discard # discard 1387 | else: 1388 | # any other bindings are invalid 1389 | error("invalid foreign binding", decl) 1390 | when defined(euwrenDumpForeignModule): 1391 | stmts.add(newCall("echo", ident"modSrc")) 1392 | stmts.add(newCall("module", vm, module, ident"modSrc")) 1393 | result = newBlockStmt(stmts) 1394 | 1395 | macro ready*(vm: Wren): untyped = 1396 | ## Arms the VM for foreign code execution. This **must** be called after 1397 | ## any and all ``foreign()`` calls are done and before you execute any code 1398 | ## that uses foreign data inside Wren, otherwise type checking will not work 1399 | ## properly. 1400 | result = newStmtList() 1401 | for id, name in typeNames: 1402 | let parents = parentTypeIds[id] 1403 | result.add(newCall(ident"addTypeInfo", vm, 1404 | newLit(id), newLit(name), newLit(parents))) 1405 | typeIds.clear() 1406 | typeNames.clear() 1407 | parentTypeIds.clear() 1408 | wrenNames.clear() 1409 | 1410 | -------------------------------------------------------------------------------- /src/euwren/private/wren.nim: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import nimterop/build 4 | import nimterop/cimport 5 | 6 | const 7 | Base = getProjectCacheDir("euwren") 8 | Src = Base/"src" 9 | Vm = Src/"vm" 10 | Optional = Src/"optional" 11 | Include = Src/"include" 12 | 13 | static: 14 | gitPull("https://github.com/liquid600pgm/wren.git", Base, """ 15 | src/vm/* 16 | src/optional/* 17 | src/include/*""", "fix-710") 18 | 19 | cIncludeDir(Include) 20 | cIncludeDir(Vm) 21 | cIncludeDir(Optional) 22 | 23 | cCompile(Vm/"wren_compiler.c") 24 | cCompile(Vm/"wren_core.c") 25 | cCompile(Vm/"wren_debug.c") 26 | cCompile(Vm/"wren_primitive.c") 27 | cCompile(Vm/"wren_utils.c") 28 | cCompile(Vm/"wren_value.c") 29 | cCompile(Vm/"wren_vm.c") 30 | cCompile(Optional/"wren_opt_meta.c") 31 | cCompile(Optional/"wren_opt_random.c") 32 | 33 | cOverride: 34 | type 35 | WrenLoadModuleFn* = proc (vm: ptr WrenVM, name: cstring): cstring {.cdecl.} 36 | WrenReallocateFn* = proc (memory: pointer, size: csize): pointer {.cdecl.} 37 | WrenResolveModuleFn* = 38 | proc (vm: ptr WrenVM, importer, name: cstring): cstring {.cdecl.} 39 | 40 | cImport(Include/"wren.h") 41 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | teuwren 2 | 3 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /tests/teuwren.nim: -------------------------------------------------------------------------------- 1 | import segfaults 2 | import unittest 3 | 4 | import euwren 5 | 6 | suite "base API": 7 | setup: 8 | var 9 | wren = newWren() 10 | vmOut = "" 11 | wren.onWrite do (str: string): 12 | vmOut.add(str) 13 | 14 | #-- 15 | # basics 16 | #-- 17 | 18 | test "running code": 19 | wren.run(""" 20 | System.print("Hello, world!") 21 | """) 22 | check vmOut == "Hello, world!\n" 23 | test "primitive variables": 24 | wren.run(""" 25 | var x = 1 26 | var str = "Hello" 27 | """) 28 | let 29 | x = wren["main", "x", int] 30 | str = wren["main", "str", string] 31 | check x == 1 32 | check str == "Hello" 33 | test "WrenRef variables and method calls": 34 | wren.run(""" 35 | class Test { 36 | static run(param) { 37 | System.print("result = " + param.toString) 38 | if (param == 42) { 39 | return "Success" 40 | } else { 41 | return "Fail" 42 | } 43 | } 44 | } 45 | """) 46 | let 47 | testClass = wren["main", "Test"] 48 | methodRun1 = wren{"run(_)"} 49 | callResult = methodRun1.call[:string](testClass, 42) 50 | check vmOut == "result = 42\n" 51 | check callResult == "Success" 52 | test "WrenError on fiber abort": 53 | expect WrenError: 54 | wren.run(""" 55 | Fiber.abort("oops!") 56 | """) 57 | expect WrenError: 58 | wren.run(""" 59 | System.print("Adding a num to a string " + 10) 60 | """) 61 | test "onLoadModule": 62 | wren.onLoadModule do (path: string) -> string: 63 | if path == "hello": 64 | result = """ 65 | class Hello { 66 | static say() { 67 | System.print("Hello!") 68 | } 69 | } 70 | """ 71 | wren.run(""" 72 | import "hello" for Hello 73 | Hello.say() 74 | """) 75 | check vmOut == "Hello!\n" 76 | test "onResolveModule": 77 | wren.onResolveModule do (importer, name: string) -> string: 78 | if importer == "main": 79 | result = "test" & name 80 | else: 81 | result = name 82 | wren.module("testthing", """ 83 | System.print("running from testthing") 84 | """) 85 | wren.run(""" 86 | import "thing" 87 | """) 88 | check vmOut == "running from testthing\n" 89 | 90 | suite "foreign()": 91 | setup: 92 | var 93 | wren = newWren() 94 | vmOut = "" 95 | wren.onWrite do (str: string): 96 | vmOut.add(str) 97 | 98 | #-- 99 | # procedures 100 | #-- 101 | 102 | test "basic procedures": 103 | var state = "didn't pass" 104 | proc goal() = 105 | state = "passed!" 106 | wren.foreign("test"): 107 | [Test]: 108 | goal 109 | wren.ready() 110 | wren.run(""" 111 | import "test" for Test 112 | Test.goal() 113 | """) 114 | check state == "passed!" 115 | test "procedure aliasing": 116 | proc getGreeting(target: string): string = 117 | result = "Hello, " & target & "!" 118 | wren.foreign("test"): 119 | [Greeting]: 120 | getGreeting -> "get" 121 | getGreeting -> `[]` 122 | wren.ready() 123 | wren.run(""" 124 | import "test" for Greeting 125 | System.print(Greeting.get("world")) 126 | System.print(Greeting["Nim"]) 127 | """) 128 | check vmOut == "Hello, world!\nHello, Nim!\n" 129 | test "procedure overloading": 130 | proc add(a, b: int): int = a + b 131 | proc add(a, b, c: int): int = a + b + c 132 | wren.foreign("test"): 133 | [Adder]: 134 | add(int, int) 135 | add(int, int, _) 136 | wren.ready() 137 | wren.run(""" 138 | import "test" for Adder 139 | System.print(Adder.add(3, 7)) 140 | System.print(Adder.add(1, 2, 3)) 141 | """) 142 | check vmOut == "10\n6\n" 143 | test "type checking": 144 | type 145 | A = ref object of RootObj 146 | a*: string 147 | B = ref object of A 148 | b*: int 149 | C = ref object 150 | proc newA(): A = A() 151 | proc newB(): B = B() 152 | proc newC(): C = C() 153 | proc printA(a: A) = vmOut.add("got A\n") 154 | proc printB(b: B) = vmOut.add("got B\n") 155 | proc printC(c: C) = vmOut.add("got C\n") 156 | wren.foreign("test"): 157 | A: 158 | *newA -> new 159 | B: 160 | *newB -> new 161 | C: 162 | *newC -> new 163 | [Test]: 164 | printA 165 | printB 166 | printC 167 | wren.ready() 168 | wren.run(""" 169 | import "test" for A, B, C, Test 170 | var a = A.new() 171 | var b = B.new() 172 | var c = C.new() 173 | a.a = "hello" 174 | b.b = 42 175 | b.a = "world" 176 | Test.printA(a) 177 | Test.printB(b) 178 | Test.printA(b) 179 | Test.printC(c) 180 | """) 181 | check vmOut == "got A\ngot B\ngot A\ngot C\n" 182 | expect WrenError: 183 | wren.run(""" 184 | var a = A.new() 185 | Test.printC(a) 186 | """) 187 | test "procedure exceptions": 188 | var success = false 189 | proc exceptionTest() = 190 | raise newException(Exception, "test error") 191 | proc ok() = success = true 192 | wren.foreign("test"): 193 | [Test]: 194 | exceptionTest 195 | ok 196 | wren.ready() 197 | expect WrenError: 198 | wren.run(""" 199 | import "test" for Test 200 | Test.exceptionTest() 201 | """) 202 | wren.run(""" 203 | var error = Fiber.new { Test.exceptionTest() }.try() 204 | if (error.startsWith("test error [Exception]")) { 205 | Test.ok() 206 | } else { 207 | System.print("fail: wanted `test error [Exception]`") 208 | System.print("got: " + error) 209 | } 210 | """) 211 | if not success: echo vmOut 212 | check success 213 | test "default param handling": 214 | var a = 2 215 | proc literalParams(x = 2): int = x + 1 216 | proc identParams(x = a): int = x + 2 217 | wren.foreign("test"): 218 | [Test]: 219 | literalParams 220 | identParams 221 | wren.ready() 222 | wren.run(""" 223 | import "test" for Test 224 | System.print(Test.literalParams(2)) 225 | System.print(Test.identParams(3)) 226 | """) 227 | test "array params": 228 | test "array params - basic": 229 | proc testArray0(x: array[4, int], doCheck: bool) = 230 | if doCheck: 231 | check x == [1, 2, 3, 4] 232 | proc testArray1(x: array[1..4, int]) = 233 | for i in 1..4: 234 | check x[i] == i 235 | wren.foreign("test1"): 236 | [Array]: 237 | testArray0 -> test0 238 | testArray1 -> test1 239 | wren.ready() 240 | wren.run(""" 241 | import "test1" for Array 242 | Array.test0([1, 2, 3, 4], true) 243 | Array.test1([1, 2, 3, 4]) 244 | """) 245 | expect WrenError: 246 | wren.run(""" 247 | Array.test0([1, 2], false) 248 | """) 249 | expect WrenError: 250 | wren.run(""" 251 | Array.test0([1, "hello", 3, 4], false) 252 | """) 253 | test "array params - matrix": 254 | proc testMatrix(x: array[3, array[3, float]]) = 255 | check x == [ 256 | [0.0, 1.0, 2.0], 257 | [1.0, 2.0, 3.0], 258 | [2.0, 3.0, 4.0] 259 | ] 260 | wren.foreign("test2"): 261 | [Matrix]: 262 | testMatrix -> test 263 | wren.ready() 264 | wren.run(""" 265 | import "test2" for Matrix 266 | Matrix.test([ 267 | [0, 1, 2], 268 | [1, 2, 3], 269 | [2, 3, 4] 270 | ]) 271 | """) 272 | test "array params - objects": 273 | type 274 | Test = object 275 | x: int 276 | proc newTest(x: int): Test = Test(x: x) 277 | proc testObjects(x: array[4, Test]) = 278 | check x == [ 279 | Test(x: 1), Test(x: 2), Test(x: 3), Test(x: 4) 280 | ] 281 | wren.foreign("test3"): 282 | Test: 283 | *newTest -> new 284 | [Objects]: 285 | testObjects -> test 286 | wren.ready() 287 | wren.run(""" 288 | import "test3" for Test, Objects 289 | Objects.test([ 290 | Test.new(1), 291 | Test.new(2), 292 | Test.new(3), 293 | Test.new(4) 294 | ]) 295 | """) 296 | test "array return type": 297 | test "array return type - simple": 298 | proc getArray(): array[4, int] = 299 | result = [1, 2, 3, 4] 300 | wren.foreign("test1"): 301 | [Array]: 302 | getArray -> get 303 | wren.ready() 304 | wren.run(""" 305 | import "test1" for Array 306 | var x = Array.get() 307 | for (a in x) { 308 | System.print(a) 309 | } 310 | """) 311 | check vmOut == "1\n2\n3\n4\n" 312 | test "array return type - matrix": 313 | proc getMatrix(): array[3, array[3, float]] = 314 | result = [ 315 | [1.0, 2.0, 3.0], 316 | [2.0, 3.0, 4.0], 317 | [3.0, 4.0, 5.0] 318 | ] 319 | wren.foreign("test2"): 320 | [Matrix]: 321 | getMatrix -> get 322 | wren.ready() 323 | wren.run(""" 324 | import "test2" for Matrix 325 | var x = Matrix.get() 326 | var result = "" 327 | for (a in x) { 328 | for (b in a) { 329 | result = result + b.toString 330 | } 331 | } 332 | System.print(result) 333 | """) 334 | check vmOut == "123234345\n" 335 | test "seq params": 336 | test "seq params - basic": 337 | proc testSeq(x: seq[int], doCheck: bool) = 338 | if doCheck: 339 | check x == @[1, 2, 3] 340 | wren.foreign("test1"): 341 | [Seq]: 342 | testSeq -> test 343 | wren.ready() 344 | wren.run(""" 345 | import "test1" for Seq 346 | Seq.test([1, 2, 3], true) 347 | """) 348 | expect WrenError: 349 | wren.run(""" 350 | Seq.test([1, "hello", 3, true], false) 351 | """) 352 | test "seq params - nested": 353 | proc testNestedSeq(x: seq[seq[int]]) = 354 | check x == @[ 355 | @[1, 2, 3, 4, 5], 356 | @[2, 3], 357 | @[5, 4, 3] 358 | ] 359 | wren.foreign("test2"): 360 | [NestedSeq]: 361 | testNestedSeq -> test 362 | wren.ready() 363 | wren.run(""" 364 | import "test2" for NestedSeq 365 | NestedSeq.test([ 366 | [1, 2, 3, 4, 5], 367 | [2, 3], 368 | [5, 4, 3] 369 | ]) 370 | """) 371 | test "inline procs": 372 | wren.foreign("test"): 373 | [Inline]: 374 | normal do (x: int) -> float: 375 | result = float(x) * 1.5 376 | ?getter do -> float: 3.14159 377 | wren.ready() 378 | wren.run(""" 379 | import "test" for Inline 380 | System.print(Inline.normal(2)) 381 | System.print(Inline.getter) 382 | """) 383 | check vmOut == "3\n3.14159\n" 384 | test "injections": 385 | wren.foreign("test"): 386 | """ 387 | class Injected { 388 | static test() { System.print("top-level") } 389 | } 390 | """ 391 | [Test]: 392 | """ 393 | static test() { System.print("in class") } 394 | """ 395 | wren.ready() 396 | wren.run(""" 397 | import "test" for Injected, Test 398 | Injected.test() 399 | Test.test() 400 | """) 401 | check vmOut == "top-level\nin class\n" 402 | 403 | #-- 404 | # objects 405 | #-- 406 | 407 | test "objects": 408 | type 409 | TestObject = object 410 | publicField*: string 411 | privateField: string 412 | proc newTestObject(): TestObject = 413 | result = TestObject(publicField: "access granted", 414 | privateField: "access denied") 415 | wren.foreign("test"): 416 | TestObject: 417 | *newTestObject -> new 418 | wren.ready() 419 | wren.run(""" 420 | import "test" for TestObject 421 | var x = TestObject.new() 422 | System.print(x.publicField) 423 | x.publicField = "hacked" 424 | System.print(x.publicField) 425 | """) 426 | check vmOut == "access granted\nhacked\n" 427 | expect WrenError: 428 | wren.run(""" 429 | import "test" for TestObject 430 | var x = TestObject.new() 431 | System.print(x.privateField) 432 | """) 433 | test "object aliasing": 434 | type 435 | ObjectWithVerboseName = object 436 | proc newObjectWithVerboseName(): ObjectWithVerboseName = 437 | result = ObjectWithVerboseName() 438 | wren.foreign("test"): 439 | ObjectWithVerboseName -> Verbose: 440 | *newObjectWithVerboseName -> new 441 | wren.ready() 442 | wren.run(""" 443 | import "test" for Verbose 444 | var x = Verbose.new() 445 | """) 446 | test "object getters": 447 | type 448 | Greeter = object 449 | target: string 450 | proc newGreeter(target: string): Greeter = Greeter(target: target) 451 | proc greeting(greeter: Greeter): string = "Hello, " & greeter.target & "!" 452 | wren.foreign("test"): 453 | Greeter: 454 | *newGreeter -> new 455 | ?greeting 456 | wren.ready() 457 | wren.run(""" 458 | import "test" for Greeter 459 | var x = Greeter.new("world") 460 | System.print(x.greeting) 461 | """) 462 | check vmOut == "Hello, world!\n" 463 | test "`var` receiver": 464 | type 465 | Counter = object 466 | count: int 467 | proc newCounter(): Counter = Counter(count: 0) 468 | proc inc(counter: var Counter) = inc(counter.count) 469 | proc count(counter: Counter): int = counter.count 470 | wren.foreign("count"): 471 | Counter: 472 | *newCounter -> new 473 | inc(var Counter) 474 | ?count 475 | wren.ready() 476 | wren.run(""" 477 | import "count" for Counter 478 | var x = Counter.new() 479 | System.print(x.count) 480 | x.inc() 481 | System.print(x.count) 482 | """) 483 | check vmOut == "0\n1\n" 484 | test "ref objects": 485 | type 486 | RefObj = ref object 487 | x: int 488 | proc newRefObj(x: int): RefObj = RefObj(x: x) 489 | proc something(): RefObj = RefObj(x: 2) 490 | wren.foreign("test"): 491 | RefObj: 492 | *newRefObj -> new 493 | something 494 | wren.ready() 495 | wren.run(""" 496 | import "test" for RefObj 497 | var x = RefObj.new(2) 498 | """) 499 | test "tuples": 500 | type 501 | Rect = tuple[x, y, width, height: float] 502 | proc checkRect(r: Rect) = 503 | check r == (x: 1.0, y: 2.0, width: 3.0, height: 4.0) 504 | wren.foreign("test"): 505 | Rect: discard 506 | [Test]: 507 | checkRect 508 | wren.ready() 509 | wren.run(""" 510 | import "test" for Rect, Test 511 | var r = Rect.new(1, 2, 3, 4) 512 | System.print(r.width * r.height) 513 | Test.checkRect(r) 514 | """) 515 | test "{.noFields.}": 516 | type 517 | Thing = object 518 | field*: float 519 | proc newThing(): Thing = Thing(field: 1) 520 | wren.foreign("test"): 521 | Thing: 522 | {.noFields.} 523 | *newThing -> new 524 | wren.ready() 525 | expect WrenError: 526 | wren.run(""" 527 | import "test" for Thing 528 | var x = Thing.new() 529 | System.print(x.field) 530 | """) 531 | test "concrete generic": 532 | type 533 | Vec2[T] = object 534 | x*, y*: T 535 | proc vec2[T](x, y: T): Vec2[T] = 536 | result = Vec2[T](x: x, y: y) 537 | proc `+`[T](a, b: Vec2[T]): Vec2[T] = 538 | result = Vec2[T](x: a.x + b.x, y: a.y + b.y) 539 | wren.foreign("test"): 540 | Vec2[float] -> Vec2f: 541 | *vec2(float, float) -> new 542 | `+`(Vec2[float], Vec2[float]) 543 | wren.ready() 544 | wren.run(""" 545 | import "test" for Vec2f 546 | 547 | var a = Vec2f.new(10, 10) 548 | var b = Vec2f.new(20, 40) 549 | var c = a + b 550 | """) 551 | 552 | #-- 553 | # enums 554 | #-- 555 | 556 | test "basic enums": 557 | type 558 | TestEnum = enum 559 | A, B, C 560 | wren.foreign("test"): 561 | TestEnum 562 | wren.ready() 563 | wren.run(""" 564 | import "test" for TestEnum 565 | System.print(TestEnum.A) 566 | System.print(TestEnum.B) 567 | System.print(TestEnum.C) 568 | """) 569 | check vmOut == "0\n1\n2\n" 570 | test "enum aliasing": 571 | type 572 | TestEnum = enum 573 | A, B, C 574 | wren.foreign("test"): 575 | TestEnum -> Test 576 | wren.ready() 577 | wren.run(""" 578 | import "test" for Test 579 | System.print(Test.A) 580 | System.print(Test.B) 581 | System.print(Test.C) 582 | """) 583 | check vmOut == "0\n1\n2\n" 584 | test "enum prefix stripping": 585 | type 586 | TestEnum = enum 587 | testA, testB, testC 588 | wren.foreign("test"): 589 | TestEnum - test 590 | wren.ready() 591 | wren.run(""" 592 | import "test" for TestEnum 593 | System.print(TestEnum.A) 594 | System.print(TestEnum.B) 595 | System.print(TestEnum.C) 596 | """) 597 | check vmOut == "0\n1\n2\n" 598 | test "enum prefix stripping with aliasing": 599 | type 600 | TestEnum = enum 601 | testA, testB, testC 602 | wren.foreign("test"): 603 | TestEnum - test -> Test 604 | wren.ready() 605 | wren.run(""" 606 | import "test" for Test 607 | System.print(Test.A) 608 | System.print(Test.B) 609 | System.print(Test.C) 610 | """) 611 | check vmOut == "0\n1\n2\n" 612 | 613 | --------------------------------------------------------------------------------