1 /* 2 FIXME: 3 overloads can be done as an object representing the overload set 4 tat opCall does the dispatch. Then other overloads can actually 5 be added more sanely. 6 7 FIXME: 8 instantiate template members when reflection with certain 9 arguments if marked right... 10 11 12 FIXME: 13 pointer to member functions can give a way to wrap things 14 15 we'll pass it an opaque object as this and it will unpack and call the method 16 17 we can also auto-generate getters and setters for properties with this method 18 19 and constructors, so the script can create class objects too 20 */ 21 22 23 /++ 24 jsvar provides a D type called [var] that works similarly to the same in Javascript. 25 26 It is weakly (even weaker than JS, frequently returning null rather than throwing on 27 an invalid operation) and dynamically typed, but interops pretty easily with D itself: 28 29 --- 30 var a = 10; 31 a ~= "20"; 32 assert(a == "1020"); 33 34 var a = function(int b, int c) { return b+c; }; 35 // note the second set of () is because of broken @property 36 assert(a()(10,20) == 30); 37 38 var a = var.emptyObject; 39 a.foo = 30; 40 assert(a["foo"] == 30); 41 42 var b = json!q{ 43 "foo":12, 44 "bar":{"hey":[1,2,3,"lol"]} 45 }; 46 47 assert(b.bar.hey[1] == 2); 48 --- 49 50 51 You can also use [var.fromJson], a static method, to quickly and easily 52 read json or [var.toJson] to write it. 53 54 Also, if you combine this with my [arsd.script] module, you get pretty 55 easy interop with a little scripting language that resembles a cross between 56 D and Javascript - just like you can write in D itself using this type. 57 58 Please note that function default arguments are NOT likely to work in the script. 59 You'd have to use a helper thing that I haven't written yet. opAssign can never 60 do it because that information is lost when it becomes a pointer. ParamDefault 61 is thus commented out for now. 62 63 64 Properties: 65 $(LIST 66 * note that @property doesn't work right in D, so the opDispatch properties 67 will require double parenthesis to call as functions. 68 69 * Properties inside a var itself are set specially: 70 obj.propName._object = new PropertyPrototype(getter, setter); 71 ) 72 73 D structs can be turned to vars, but it is a copy. 74 75 Wrapping D native objects is coming later, the current ways suck. I really needed 76 properties to do them sanely at all, and now I have it. A native wrapped object will 77 also need to be set with _object prolly. 78 79 Author: Adam D Ruppe 80 81 History: Started in July 2013. 82 +/ 83 module arsd.jsvar; 84 85 import arsd.core; 86 87 version=new_std_json; 88 89 static import std.array; 90 import std.traits; 91 import std.conv; 92 import std.json; 93 94 version(jsvar_throw) 95 /++ 96 Variable to decide if jsvar throws on certain invalid 97 operations or continues on propagating null vars. 98 +/ 99 bool jsvar_throw = true; 100 else 101 /// ditto 102 bool jsvar_throw = false; 103 104 // uda for wrapping classes 105 enum scriptable = "arsd_jsvar_compatible"; 106 107 /* 108 PrototypeObject FIXME: 109 make undefined variables reaction overloadable in PrototypeObject, not just a switch 110 111 script FIXME: 112 113 the Expression should keep scriptFilename and lineNumber around for error messages 114 115 it should consistently throw on missing semicolons 116 117 *) in operator 118 119 *) nesting comments, `` string literals 120 *) opDispatch overloading 121 *) properties???// 122 a.prop on the rhs => a.prop() 123 a.prop on the lhs => a.prop(rhs); 124 if opAssign, it can just do a.prop(a.prop().opBinary!op(rhs)); 125 126 But, how do we mark properties in var? Can we make them work this way in D too? 127 0) add global functions to the object (or at least provide a convenience function to make a pre-populated global object) 128 1) ensure operator precedence is sane 129 2) a++ would prolly be nice, and def -a 130 4) switches? 131 10) __FILE__ and __LINE__ as default function arguments should work like in D 132 16) stack traces on script exceptions 133 17) an exception type that we can create in the script 134 135 14) import???????/ it could just attach a particular object to the local scope, and the module decl just giving the local scope a name 136 there could be a super-global object that is the prototype of the "global" used here 137 then you import, and it pulls moduleGlobal.prototype = superGlobal.modulename... or soemthing. 138 139 to get the vars out in D, you'd have to be aware of this, since you pass the superglobal 140 hmmm maybe not worth it 141 142 though maybe to export vars there could be an explicit export namespace or something. 143 144 145 6) gotos? labels? labeled break/continue? 146 18) what about something like ruby's blocks or macros? parsing foo(arg) { code } is easy enough, but how would we use it? 147 148 var FIXME: 149 150 user defined operator overloading on objects, including opCall, opApply, and more 151 flesh out prototype objects for Array, String, and Function 152 153 looserOpEquals 154 155 it would be nice if delegates on native types could work 156 */ 157 158 static if(__VERSION__ <= 2076) { 159 // compatibility shims with gdc 160 enum JSONType { 161 object = JSON_TYPE.OBJECT, 162 null_ = JSON_TYPE.NULL, 163 false_ = JSON_TYPE.FALSE, 164 true_ = JSON_TYPE.TRUE, 165 integer = JSON_TYPE.INTEGER, 166 float_ = JSON_TYPE.FLOAT, 167 array = JSON_TYPE.ARRAY, 168 string = JSON_TYPE.STRING, 169 uinteger = JSON_TYPE.UINTEGER 170 } 171 } 172 173 174 /* 175 Script notes: 176 177 the one type is var. It works just like the var type in D from arsd.jsvar. 178 (it might be fun to try to add other types, and match D a little better here! We could allow implicit conversion to and from var, but not on the other types, they could get static checking. But for now it is only var. BTW auto is an alias for var right now) 179 180 There is no comma operator, but you can use a scope as an expression: a++, b++; can be written as {a++;b++;} 181 */ 182 183 version(test_script) 184 struct Foop { 185 int a = 12; 186 string n = "hate"; 187 void speak() { writeln(n, " ", a); n = "love"; writeln(n, " is what it is now"); } 188 void speak2() { writeln("speak2 ", n, " ", a); } 189 } 190 version(test_script) 191 void main() { 192 import arsd.script; 193 writeln(interpret("x*x + 3*x;", var(["x":3]))); 194 195 { 196 var a = var.emptyObject; 197 a.qweq = 12; 198 } 199 200 // the WrappedNativeObject is disgusting 201 // but works. sort of. 202 /* 203 Foop foop2; 204 205 var foop; 206 foop._object = new WrappedNativeObject!Foop(foop2); 207 208 foop.speak()(); 209 foop.a = 25; 210 writeln(foop.n); 211 foop.speak2()(); 212 return; 213 */ 214 215 import arsd.script; 216 struct Test { 217 int a = 10; 218 string name = "ten"; 219 } 220 221 auto globals = var.emptyObject; 222 globals.lol = 100; 223 globals.rofl = 23; 224 225 globals.arrtest = var.emptyArray; 226 227 globals.write._function = (var _this, var[] args) { 228 string s; 229 foreach(a; args) 230 s ~= a.get!string; 231 writeln("script said: ", s); 232 return var(null); 233 }; 234 235 // call D defined functions in script 236 globals.func = (var a, var b) { writeln("Hello, world! You are : ", a, " and ", b); }; 237 238 globals.ex = () { throw new ScriptRuntimeException("test", 1); }; 239 240 globals.fun = { return var({ writeln("hello inside!"); }); }; 241 242 import std.file; 243 writeln(interpret(readText("scripttest_code.d"), globals)); 244 245 globals.ten = 10.0; 246 globals.five = 5.0; 247 writeln(interpret(q{ 248 var a = json!q{ }; 249 a.b = json!q{ }; 250 a.b.c = 10; 251 a; 252 }, globals)); 253 254 /* 255 globals.minigui = json!q{}; 256 import arsd.minigui; 257 globals.minigui.createWindow = { 258 var v; 259 auto mw = new MainWindow(); 260 v._object = new OpaqueNativeObject!(MainWindow)(mw); 261 v.loop = { mw.loop(); }; 262 return v; 263 }; 264 */ 265 266 repl(globals); 267 268 writeln("BACK IN D!"); 269 globals.c()(10); // call script defined functions in D (note: this runs the interpreter) 270 271 //writeln(globals._getMember("lol", false)); 272 return; 273 274 var k,l ; 275 276 var j = json!q{ 277 "hello": { 278 "data":[1,2,"giggle",4] 279 }, 280 "world":20 281 }; 282 283 writeln(j.hello.data[2]); 284 285 286 Test t; 287 var rofl = t; 288 writeln(rofl.name); 289 writeln(rofl.a); 290 291 rofl.a = "20"; 292 rofl.name = "twenty"; 293 294 t = rofl.get!Test; 295 writeln(t); 296 297 var a1 = 10; 298 a1 -= "5"; 299 a1 /= 2; 300 301 writeln(a1); 302 303 var a = 10; 304 var b = 20; 305 a = b; 306 307 b = 30; 308 a += 100.2; 309 writeln(a); 310 311 var c = var.emptyObject; 312 c.a = b; 313 314 var d = c; 315 d.b = 50; 316 317 writeln(c.b); 318 319 writeln(d.toJson()); 320 321 var e = a + b; 322 writeln(a, " + ", b, " = ", e); 323 324 e = function(var lol) { 325 writeln("hello with ",lol,"!"); 326 return lol + 10; 327 }; 328 329 writeln(e("15")); 330 331 if(var("ass") > 100) 332 writeln(var("10") / "3"); 333 } 334 335 template json(string s) { 336 // ctfe doesn't support the unions std.json uses :( 337 //enum json = var.fromJsonObject(s); 338 339 // FIXME we should at least validate string s at compile time 340 var json() { 341 return var.fromJson("{" ~ s ~ "}"); 342 } 343 } 344 345 // literals 346 347 // var a = varArray(10, "cool", 2); 348 // assert(a[0] == 10); assert(a[1] == "cool"); assert(a[2] == 2); 349 var varArray(T...)(T t) { 350 var a = var.emptyArray; 351 foreach(arg; t) 352 a ~= var(arg); 353 return a; 354 } 355 356 // var a = varObject("cool", 10, "bar", "baz"); 357 // assert(a.cool == 10 && a.bar == "baz"); 358 var varObject(T...)(T t) { 359 var a = var.emptyObject; 360 361 string lastString; 362 foreach(idx, arg; t) { 363 static if(idx % 2 == 0) { 364 lastString = arg; 365 } else { 366 assert(lastString !is null); 367 a[lastString] = arg; 368 lastString = null; 369 } 370 } 371 return a; 372 } 373 374 375 private double stringToNumber(string s) { 376 double r; 377 try { 378 r = to!double(s); 379 } catch (Exception e) { 380 r = double.nan; 381 } 382 383 return r; 384 } 385 386 private bool doubleIsInteger(double r) { 387 return (r == cast(long) r); 388 } 389 390 // helper template for operator overloading 391 private var _op(alias _this, alias this2, string op, T)(T t) if(op == "~") { 392 static if(is(T == var)) { 393 if(t.payloadType() == var.Type.Array) 394 return _op!(_this, this2, op)(t._payload._array); 395 else if(t.payloadType() == var.Type.String) 396 return _op!(_this, this2, op)(t._payload._string); 397 //else 398 //return _op!(_this, this2, op)(t.get!string); 399 } 400 401 if(this2.payloadType() == var.Type.Array) { 402 auto l = this2._payload._array; 403 static if(isArray!T && !isSomeString!T) 404 foreach(item; t) 405 l ~= var(item); 406 else 407 l ~= var(t); 408 409 _this._type = var.Type.Array; 410 _this._payload._array = l; 411 return _this; 412 } else if(this2.payloadType() == var.Type.String) { 413 auto l = this2._payload._string; 414 l ~= var(t).get!string; // is this right? 415 _this._type = var.Type.String; 416 _this._payload._string = l; 417 return _this; 418 } else { 419 auto l = this2.get!string; 420 l ~= var(t).get!string; 421 _this._type = var.Type.String; 422 _this._payload._string = l; 423 return _this; 424 } 425 426 assert(0); 427 428 } 429 430 // FIXME: maybe the bitops should be moved out to another function like ~ is 431 private var _op(alias _this, alias this2, string op, T)(T t) if(op != "~") { 432 static if(is(T == var)) { 433 if(t.payloadType() == var.Type.Integral) 434 return _op!(_this, this2, op)(t._payload._integral); 435 if(t.payloadType() == var.Type.Floating) 436 return _op!(_this, this2, op)(t._payload._floating); 437 if(t.payloadType() == var.Type.String) 438 return _op!(_this, this2, op)(t._payload._string); 439 throw new Exception("Attempted invalid operator `" ~ op ~ "` on variable of type " ~ to!string(t.payloadType())); 440 } else { 441 if(this2.payloadType() == var.Type.Integral) { 442 auto l = this2._payload._integral; 443 static if(isIntegral!T) { 444 mixin("l "~op~"= t;"); 445 _this._type = var.Type.Integral; 446 _this._payload._integral = l; 447 return _this; 448 } else static if(isFloatingPoint!T) { 449 static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") { 450 this2._type = var.Type.Integral; 451 long f = l; 452 mixin("f "~op~"= cast(long) t;"); 453 _this._type = var.Type.Integral; 454 _this._payload._integral = f; 455 } else { 456 this2._type = var.Type.Floating; 457 double f = l; 458 mixin("f "~op~"= t;"); 459 _this._type = var.Type.Floating; 460 _this._payload._floating = f; 461 } 462 return _this; 463 } else static if(isSomeString!T) { 464 auto rhs = stringToNumber(t); 465 if(doubleIsInteger(rhs)) { 466 mixin("l "~op~"= cast(long) rhs;"); 467 _this._type = var.Type.Integral; 468 _this._payload._integral = l; 469 } else{ 470 static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") { 471 long f = l; 472 mixin("f "~op~"= cast(long) rhs;"); 473 _this._type = var.Type.Integral; 474 _this._payload._integral = f; 475 } else { 476 double f = l; 477 mixin("f "~op~"= rhs;"); 478 _this._type = var.Type.Floating; 479 _this._payload._floating = f; 480 } 481 } 482 return _this; 483 484 } 485 } else if(this2.payloadType() == var.Type.Floating) { 486 auto f = this._payload._floating; 487 488 static if(isIntegral!T || isFloatingPoint!T) { 489 static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") { 490 long argh = cast(long) f; 491 mixin("argh "~op~"= cast(long) t;"); 492 _this._type = var.Type.Integral; 493 _this._payload._integral = argh; 494 } else { 495 mixin("f "~op~"= t;"); 496 _this._type = var.Type.Floating; 497 _this._payload._floating = f; 498 } 499 return _this; 500 } else static if(isSomeString!T) { 501 auto rhs = stringToNumber(t); 502 503 static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") { 504 long pain = cast(long) f; 505 mixin("pain "~op~"= cast(long) rhs;"); 506 _this._type = var.Type.Integral; 507 _this._payload._floating = pain; 508 } else { 509 mixin("f "~op~"= rhs;"); 510 _this._type = var.Type.Floating; 511 _this._payload._floating = f; 512 } 513 return _this; 514 } else static assert(0); 515 } else if(this2.payloadType() == var.Type.String) { 516 static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") { 517 long r = cast(long) stringToNumber(this2._payload._string); 518 long rhs; 519 } else { 520 double r = stringToNumber(this2._payload._string); 521 double rhs; 522 } 523 524 static if(isSomeString!T) { 525 rhs = cast(typeof(rhs)) stringToNumber(t); 526 } else { 527 rhs = to!(typeof(rhs))(t); 528 } 529 530 mixin("r " ~ op ~ "= rhs;"); 531 532 static if(is(typeof(r) == double)) { 533 _this._type = var.Type.Floating; 534 _this._payload._floating = r; 535 } else static if(is(typeof(r) == long)) { 536 _this._type = var.Type.Integral; 537 _this._payload._integral = r; 538 } else static assert(0); 539 return _this; 540 } else { 541 // the operation is nonsensical, we should throw or ignore it 542 var i = 0; 543 return i; 544 } 545 } 546 547 assert(0); 548 } 549 550 // a plain `compiles(T(var))` check can be triggered by an implicit cast 551 // which we don't want due to endless recursion so this more specific check 552 // are better than that anyway 553 private template hasVarConstructor(T) { 554 bool helper() { 555 static if(is(T == struct) || is(T == class)) 556 static if(__traits(hasMember, T, "__ctor")) 557 foreach(overload; __traits(getOverloads, T, "__ctor")) 558 static if(is(typeof(overload) Params == __parameters)) 559 static if(Params.length == 1) 560 static if(is(Params[0] == var)) 561 return true; 562 return false; 563 } 564 565 enum bool hasVarConstructor = helper(); 566 } 567 568 unittest { 569 assert(hasVarConstructor!string == false); 570 571 static struct A { 572 this(var a) {} 573 } 574 575 static struct B { 576 this(int a) {} 577 } 578 579 assert(hasVarConstructor!A == true); 580 assert(hasVarConstructor!B == false); 581 } 582 583 584 /// 585 struct var { 586 @implicit 587 public this(T)(T t) { 588 static if(is(T == var)) 589 this = t; 590 else 591 this.opAssign(t); 592 } 593 594 T opImplicitCast(T)() { 595 return this.get!T; 596 } 597 598 // used by the script interpreter... does a .dup on array, new on class if possible, otherwise copies members. 599 public var _copy_new() { 600 if(payloadType() == Type.Object) { 601 var cp; 602 if(this._payload._object !is null) { 603 auto po = this._payload._object.new_(null); 604 cp._object = po; 605 } 606 return cp; 607 } else if(payloadType() == Type.Array) { 608 var cp; 609 cp = this._payload._array.dup; 610 return cp; 611 } else { 612 return this._copy(); 613 } 614 } 615 616 public var _copy() { 617 final switch(payloadType()) { 618 case Type.Integral: 619 case Type.Boolean: 620 case Type.Floating: 621 case Type.Function: 622 case Type.String: 623 // since strings are immutable, we can pretend they are value types too 624 return this; // value types don't need anything special to be copied 625 626 case Type.Array: 627 var cp; 628 cp = this._payload._array[]; 629 return cp; 630 case Type.Object: 631 var cp; 632 if(this._payload._object !is null) 633 cp._object = this._payload._object.copy; 634 return cp; 635 } 636 } 637 638 /// `if(some_var)` will call this and give behavior based on the dynamic type. Shouldn't be too surprising. 639 public bool opCast(T:bool)() { 640 final switch(this._type) { 641 case Type.Object: 642 return this._payload._object !is null; 643 case Type.Array: 644 return this._payload._array.length != 0; 645 case Type.String: 646 return this._payload._string.length != 0; 647 case Type.Integral: 648 return this._payload._integral != 0; 649 case Type.Floating: 650 return this._payload._floating != 0; 651 case Type.Boolean: 652 return this._payload._boolean; 653 case Type.Function: 654 return this._payload._function !is null; 655 } 656 } 657 658 /// You can foreach over a var. 659 public int opApply(scope int delegate(ref var) dg) { 660 foreach(i, item; this) 661 if(auto result = dg(item)) 662 return result; 663 return 0; 664 } 665 666 /// ditto 667 public int opApply(scope int delegate(var, ref var) dg) { 668 if(this.payloadType() == Type.Array) { 669 foreach(i, ref v; this._payload._array) 670 if(auto result = dg(var(i), v)) 671 return result; 672 } else if(this.payloadType() == Type.Object && this._payload._object !is null) { 673 // FIXME: if it offers input range primitives, we should use them 674 // FIXME: user defined opApply on the object 675 foreach(k, ref v; this._payload._object) 676 if(auto result = dg(var(k), v)) 677 return result; 678 } else if(this.payloadType() == Type.String) { 679 // this is to prevent us from allocating a new string on each character, hopefully limiting that massively 680 static immutable string chars = makeAscii!(); 681 682 foreach(i, dchar c; this._payload._string) { 683 var lol = ""; 684 if(c < 128) 685 lol._payload._string = chars[c .. c + 1]; 686 else 687 lol._payload._string = to!string(""d ~ c); // blargh, how slow can we go? 688 if(auto result = dg(var(i), lol)) 689 return result; 690 } 691 } 692 // throw invalid foreach aggregate 693 694 return 0; 695 } 696 697 698 /// Alias for [get]. e.g. `string s = cast(string) v;` 699 public T opCast(T)() { 700 return this.get!T; 701 } 702 703 /// Calls [get] for a type automatically. `int a; var b; b.putInto(a);` will auto-convert to `int`. 704 public auto ref putInto(T)(ref T t) { 705 return t = this.get!T; 706 } 707 708 /++ 709 Assigns a value to the var. It will do necessary implicit conversions 710 and wrapping. 711 712 You can make a method `toArsdJsvar` on your own objects to override this 713 default. It should return a [var]. 714 715 History: 716 On April 20, 2020, I changed the default mode for class assignment 717 to [wrapNativeObject]. Previously it was [wrapOpaquely]. 718 719 With the new [wrapNativeObject] behavior, you can mark methods 720 @[scriptable] to expose them to the script. 721 +/ 722 public var opAssign(T)(T t) if(!is(T == var)) { 723 import std.typecons; 724 static if(__traits(compiles, this = t.toArsdJsvar())) { 725 static if(__traits(compiles, t is null)) { 726 if(t is null) 727 this = null; 728 else 729 this = t.toArsdJsvar(); 730 } else 731 this = t.toArsdJsvar(); 732 } else static if(is(T : PrototypeObject)) { 733 // support direct assignment of pre-made implementation objects 734 // so prewrapped stuff can be easily passed. 735 this._type = Type.Object; 736 this._payload._object = t; 737 } else static if(is(T == Nullable!N, N)) { 738 if(t.isNull()) 739 this = null; 740 else 741 this = t.get(); 742 } else static if(is(T == enum)) { 743 this._type = Type.String; 744 this._payload._string = to!string(t); 745 } else static if(isFloatingPoint!T) { 746 this._type = Type.Floating; 747 this._payload._floating = t; 748 } else static if(isIntegral!T) { 749 this._type = Type.Integral; 750 this._payload._integral = t; 751 } else static if(isCallable!T) { 752 this._type = Type.Function; 753 static if(is(T == typeof(this._payload._function))) { 754 this._payload._function = t; 755 } else 756 this._payload._function = delegate var(var _this, var[] args) { 757 var ret; 758 759 ParameterTypeTuple!T fargs; 760 761 // default args? nope they can't work cuz it is assigning a function pointer by here. alas. 762 enum lol = static_foreach(fargs.length, 1, -1, 763 `t(`,``,` < args.length ? args[`,`].get!(typeof(fargs[`,`])) : typeof(fargs[`,`]).init,`,`)`); 764 //`t(`,``,` < args.length ? args[`,`].get!(typeof(fargs[`,`])) : ParamDefault!(T, `,`)(),`,`)`); 765 /+ 766 foreach(idx, a; fargs) { 767 if(idx == args.length) 768 break; 769 cast(Unqual!(typeof(a))) fargs[idx] = args[idx].get!(typeof(a)); 770 } 771 +/ 772 773 static if(is(ReturnType!t == void)) { 774 //t(fargs); 775 mixin(lol ~ ";"); 776 } else { 777 //ret = t(fargs); 778 ret = mixin(lol); 779 } 780 781 return ret; 782 }; 783 } else static if(isSomeString!T) { 784 this._type = Type.String; 785 this._payload._string = to!string(t); 786 } else static if(is(T == class) || is(T == interface)) { 787 if(t !is null && (cast(Object) t) is null) 788 throw new Exception("Unsupported class or interface"); 789 this._type = Type.Object; 790 this._payload._object = t is null ? null : wrapNativeObject(t); 791 } else static if(.isScriptableOpaque!T) { 792 // auto-wrap other classes with reference semantics 793 this._type = Type.Object; 794 this._payload._object = wrapOpaquely(t); 795 } else static if(is(T == struct) || isAssociativeArray!T) { 796 // copy structs and assoc arrays by value into a var object 797 this._type = Type.Object; 798 auto obj = new PrototypeObject(); 799 this._payload._object = obj; 800 801 static if(is(T == struct)) 802 foreach(member; __traits(allMembers, T)) { 803 static if(__traits(compiles, __traits(getMember, t, member))) { 804 static if(is(typeof(__traits(getMember, t, member)) == function)) { 805 // skipping these because the delegate we get isn't going to work anyway; the object may be dead and certainly won't be updated 806 //this[member] = &__traits(getMember, proxyObject, member); 807 808 // but for simple toString, I'll allow it by recreating the object on demand 809 // and then calling the original function. (I might be able to do that for more but 810 // idk, just doing simple thing first) 811 static if(member == "toString" && is(typeof(&__traits(getMember, t, member)) == string delegate())) { 812 this[member]._function = delegate(var _this, var[] args) { 813 auto val = _this.get!T; 814 return var(val.toString()); 815 }; 816 } 817 } else static if(is(typeof(__traits(getMember, t, member)))) { 818 static if(!is(typeof(__traits(getMember, t, member)) == void)) 819 this[member] = __traits(getMember, t, member); 820 } 821 } 822 } else { 823 // assoc array 824 foreach(l, v; t) { 825 this[var(l)] = var(v); 826 } 827 } 828 } else static if(isArray!T) { 829 this._type = Type.Array; 830 var[] arr; 831 arr.length = t.length; 832 static if(!is(T == void[])) // we can't append a void array but it is nice to support x = []; 833 foreach(i, item; t) 834 arr[i] = var(item); 835 this._payload._array = arr; 836 } else static if(is(T == bool)) { 837 this._type = Type.Boolean; 838 this._payload._boolean = t; 839 } else static if(isSomeChar!T) { 840 this._type = Type.String; 841 this._payload._string = ""; 842 import std.utf; 843 char[4] ugh; 844 auto size = encode(ugh, t); 845 this._payload._string = ugh[0..size].idup; 846 }// else static assert(0, "unsupported type"); 847 848 return this; 849 } 850 851 public size_t opDollar() { 852 return this.length().get!size_t; 853 } 854 855 public var opOpAssign(string op, T)(T t) { 856 if(payloadType() == Type.Object) { 857 if(auto pt = cast(PropertyPrototype) this._payload._object) { 858 auto propValue = pt.get; 859 auto result = propValue.opOpAssign!(op)(t); 860 861 pt.set(result); 862 863 return result; 864 } else if(this._payload._object !is null) { 865 var* operator = this._payload._object._peekMember("opOpAssign", true); 866 if(operator !is null && operator._type == Type.Function) 867 return operator.call(this, op, t); 868 } 869 } 870 871 return _op!(this, this, op, T)(t); 872 } 873 874 public var opUnary(string op : "-")() { 875 static assert(op == "-"); 876 final switch(payloadType()) { 877 case Type.Object: 878 case Type.Array: 879 case Type.Boolean: 880 case Type.String: 881 case Type.Function: 882 assert(0); // FIXME 883 //break; 884 case Type.Integral: 885 return var(-this.get!long); 886 case Type.Floating: 887 return var(-this.get!double); 888 } 889 } 890 891 public var opBinary(string op, T)(T t) { 892 var n; 893 if(payloadType() == Type.Object) { 894 if(this._payload._object is null) 895 return var(null); 896 var* operator = this._payload._object._peekMember("opBinary", true); 897 if(operator !is null && operator._type == Type.Function) { 898 return operator.call(this, op, t); 899 } 900 } 901 return _op!(n, this, op, T)(t); 902 } 903 904 public var opBinaryRight(string op, T)(T s) { 905 return var(s).opBinary!op(this); 906 } 907 908 // this in foo 909 public var* opBinary(string op : "in", T)(T s) { 910 var rhs = var(s); 911 return rhs.opBinaryRight!"in"(this); 912 } 913 914 // foo in this 915 public var* opBinaryRight(string op : "in", T)(T s) { 916 // this needs to be an object 917 if(this._object is null) 918 return null; 919 return var(s).get!string in this._object._properties; 920 } 921 922 public var apply(var _this, var[] args) { 923 if(this.payloadType() == Type.Function) { 924 if(this._payload._function is null) { 925 if(jsvar_throw) 926 throw new DynamicTypeException(this, Type.Function); 927 else 928 return var(null); 929 } 930 return this._payload._function(_this, args); 931 } else if(this.payloadType() == Type.Object) { 932 if(this._payload._object is null) { 933 if(jsvar_throw) 934 throw new DynamicTypeException(this, Type.Function); 935 else 936 return var(null); 937 } 938 var* operator = this._payload._object._peekMember("opCall", true); 939 if(operator !is null && operator._type == Type.Function) 940 return operator.apply(_this, args); 941 } 942 943 if(this.payloadType() == Type.Integral || this.payloadType() == Type.Floating) { 944 if(args.length) 945 return var(this.get!double * args[0].get!double); 946 else 947 return this; 948 } else if(jsvar_throw) { 949 throw new DynamicTypeException(this, Type.Function); 950 } 951 952 //return this; 953 return var(null); 954 } 955 956 public var call(T...)(var _this, T t) { 957 var[] args; 958 foreach(a; t) { 959 args ~= var(a); 960 } 961 return this.apply(_this, args); 962 } 963 964 public var opCall(T...)(T t) { 965 return this.call(this, t); 966 } 967 968 /* 969 public var applyWithMagicLocals(var _this, var[] args, var[string] magicLocals) { 970 971 } 972 */ 973 974 public string toString() { 975 return this.get!string; 976 } 977 978 public T getWno(T)() { 979 if(payloadType == Type.Object) { 980 if(auto wno = cast(WrappedNativeObject) this._payload._object) { 981 auto no = cast(T) wno.getObject(); 982 if(no !is null) 983 return no; 984 } 985 } 986 return null; 987 } 988 989 /++ 990 Gets the var converted to type `T` as best it can. `T` may be constructed 991 from `T.fromJsVar`, or through type conversions (coercing as needed). If 992 `T` happens to be a struct, it will automatically introspect to convert 993 the var object member-by-member. 994 995 History: 996 On April 21, 2020, I changed the behavior of 997 998 --- 999 var a = null; 1000 string b = a.get!string; 1001 --- 1002 1003 Previously, `b == "null"`, which would print the word 1004 when writeln'd. Now, `b is null`, which prints the empty string, 1005 which is a bit less user-friendly, but more consistent with 1006 converting to/from D strings in general. 1007 1008 If you are printing, you can check `a.get!string is null` and print 1009 null at that point if you like. 1010 1011 I also wrote the first draft of this documentation at that time, 1012 even though the function has been public since the beginning. 1013 1014 On January 1, 2021, I changed `get!some_struct` to call properties 1015 on the var, if a member looks like a function or object, to try to 1016 get plain-old-data out. Since the functions are only ever put there 1017 by you or by you allowing script, I don't feel too bad about it, but 1018 it still might not be ideal for all circumstances, so idk if I'll leave 1019 it this way or not. 1020 1021 One thing it helps for though is taking scripted subclasses back into D 1022 structs, since the parent class thing is likely to be virtual properties. 1023 And having that just work in argument lists is really cool... 1024 1025 Search function for the comment "property getter support" to see the impl. 1026 +/ 1027 public T get(T)() if(!is(T == void)) { 1028 import std.typecons; 1029 static if(is(T == var)) { 1030 return this; 1031 } else static if(__traits(compiles, T.fromJsVar(var.init))) { 1032 return T.fromJsVar(this); 1033 } else static if(hasVarConstructor!T && __traits(compiles, T(this))) { 1034 return T(this); 1035 } else static if(hasVarConstructor!T && __traits(compiles, new T(this))) { 1036 return new T(this); 1037 } else static if(is(T == Nullable!N, N)) { 1038 if(payloadType == Type.Object && this._payload._object is null) 1039 return T.init; 1040 else 1041 return nullable(this.get!N); 1042 } else 1043 final switch(payloadType) { 1044 case Type.Boolean: 1045 static if(is(T == bool)) 1046 return this._payload._boolean; 1047 else static if(isFloatingPoint!T || isIntegral!T) 1048 return cast(T) (this._payload._boolean ? 1 : 0); // the cast is for enums, I don't like this so FIXME 1049 else static if(isSomeString!T) 1050 return to!T(this._payload._boolean ? "true" : "false"); 1051 else 1052 return T.init; 1053 case Type.Object: 1054 static if(isAssociativeArray!T) { 1055 T ret; 1056 if(this._payload._object !is null) 1057 foreach(k, v; this._payload._object._properties) 1058 ret[to!(KeyType!T)(k)] = v.get!(ValueType!T); 1059 1060 return ret; 1061 } else static if(is(T : PrototypeObject)) { 1062 // they are requesting an implementation object, just give it to them 1063 return cast(T) this._payload._object; 1064 } else static if(isScriptableOpaque!(Unqual!T)) { 1065 if(auto wno = cast(WrappedOpaque!(Unqual!T)) this._payload._object) { 1066 return wno.wrapping(); 1067 } 1068 static if(is(T == R*, R)) 1069 if(auto wno = cast(WrappedOpaque!(Unqual!(R))) this._payload._object) { 1070 return wno.wrapping(); 1071 } 1072 throw new DynamicTypeException(this, Type.Object); // FIXME: could be better 1073 } else static if(is(T == struct) || is(T == class) || is(T == interface)) { 1074 // first, we'll try to give them back the native object we have, if we have one 1075 static if(is(T : Object) || is(T == interface)) { 1076 auto t = this; 1077 // need to walk up the prototype chain too 1078 while(t != null) { 1079 assert(t.payloadType == Type.Object); 1080 if(auto wno = cast(WrappedNativeObject) t._payload._object) { 1081 auto no = cast(T) wno.getObject(); 1082 1083 if(no !is null) { 1084 auto sc = cast(ScriptableSubclass) no; 1085 if(sc !is null) { 1086 sc.setScriptVar(this); 1087 } 1088 1089 return no; 1090 } 1091 } 1092 t = t.prototype; 1093 } 1094 1095 // FIXME: this is kinda weird. 1096 return null; 1097 } else { 1098 1099 // failing that, generic struct or class getting: try to fill in the fields by name 1100 T t; 1101 bool initialized = true; 1102 static if(is(T == class)) { 1103 static if(__traits(compiles, new T())) { 1104 t = new T(); 1105 } else { 1106 initialized = false; 1107 } 1108 } 1109 1110 1111 if(initialized) 1112 foreach(i, a; t.tupleof) { 1113 var possibility = this[t.tupleof[i].stringof[2..$]]; 1114 // FIXME: so there is the possibility of getting some data getting all caught 1115 // up in a script function doing weird things. If I can prevent that, I'd like to... 1116 // but it is also really useful for this to work for some scenarios... 1117 static if(!is(typeof(a) == return)) // if it is callable, just assign the func ref 1118 if(isCallableJsvarObject(possibility)) 1119 possibility = possibility.apply(this, null); // crude approximation of property getter support 1120 cast(Unqual!(typeof((a)))) t.tupleof[i] = possibility.get!(typeof(a)); 1121 } 1122 1123 return t; 1124 } 1125 } else static if(isSomeString!T) { 1126 if(this._object !is null) 1127 return to!T(this._object.toString()); 1128 return null;// "null"; 1129 } else 1130 return T.init; 1131 case Type.Integral: 1132 static if(isFloatingPoint!T || isIntegral!T) 1133 return to!T(this._payload._integral); 1134 else static if(isSomeString!T) 1135 return to!T(this._payload._integral); 1136 else 1137 return T.init; 1138 case Type.Floating: 1139 static if(isFloatingPoint!T || isIntegral!T) 1140 return to!T(this._payload._floating); 1141 else static if(isSomeString!T) 1142 return to!T(this._payload._floating); 1143 else 1144 return T.init; 1145 case Type.String: 1146 static if(__traits(compiles, to!T(this._payload._string))) { 1147 try { 1148 return to!T(this._payload._string); 1149 } catch (Exception e) { return T.init; } 1150 } else 1151 return T.init; 1152 case Type.Array: 1153 import std.range; 1154 auto pl = this._payload._array; 1155 static if(isSomeString!T) { 1156 return to!T(pl); 1157 } else static if(is(T == E[N], E, size_t N)) { 1158 T ret; 1159 foreach(i; 0 .. N) { 1160 if(i >= pl.length) 1161 break; 1162 ret[i] = pl[i].get!E; 1163 } 1164 return ret; 1165 } else static if(is(T == E[], E)) { 1166 T ret; 1167 static if(is(ElementType!T == void)) { 1168 static assert(0, "try wrapping the function to get rid of void[] args"); 1169 //alias getType = ubyte; 1170 } else 1171 alias getType = ElementType!T; 1172 foreach(item; pl) 1173 ret ~= item.get!(getType); 1174 return ret; 1175 } else 1176 return T.init; 1177 // is it sane to translate anything else? 1178 case Type.Function: 1179 static if(isSomeString!T) { 1180 return to!T("<function>"); 1181 } else static if(isDelegate!T) { 1182 // making a local copy because otherwise the delegate might refer to a struct on the stack and get corrupted later or something 1183 auto func = this._payload._function; 1184 1185 // the static helper lets me pass specific variables to the closure 1186 static T helper(typeof(func) func) { 1187 return delegate ReturnType!T (ParameterTypeTuple!T args) { 1188 var[] arr; 1189 foreach(arg; args) 1190 arr ~= var(arg); 1191 var ret = func(var(null), arr); 1192 static if(is(ReturnType!T == void)) 1193 return; 1194 else 1195 return ret.get!(ReturnType!T); 1196 }; 1197 } 1198 1199 return helper(func); 1200 1201 } else 1202 return T.init; 1203 // FIXME: we just might be able to do better for both of these 1204 //break; 1205 } 1206 } 1207 1208 public T get(T)() if(is(T == void)) {} 1209 1210 public T nullCoalesce(T)(T t) { 1211 if(_type == Type.Object && _payload._object is null) 1212 return t; 1213 return this.get!T; 1214 } 1215 1216 public double opCmp(T)(T t) { 1217 auto f = this.get!double; 1218 static if(is(T == var)) 1219 auto r = t.get!double; 1220 else 1221 auto r = t; 1222 return f - r; 1223 } 1224 1225 public bool opEquals(T)(T t) { 1226 return this.opEquals(var(t)); 1227 } 1228 1229 public bool opEquals(T:var)(T t) const { 1230 // int and float can implicitly convert 1231 if(this._type == Type.Integral && t._type == Type.Floating) 1232 return _payload._integral == t._payload._floating; 1233 if(t._type == Type.Integral && this._type == Type.Floating) 1234 return t._payload._integral == this._payload._floating; 1235 1236 // but the others are kinda strict 1237 // FIXME: should this be == or === ? 1238 1239 if(this._type != t._type) 1240 return false; 1241 final switch(this._type) { 1242 case Type.Object: 1243 return _payload._object is t._payload._object; 1244 case Type.Integral: 1245 return _payload._integral == t._payload._integral; 1246 case Type.Boolean: 1247 return _payload._boolean == t._payload._boolean; 1248 case Type.Floating: 1249 return _payload._floating == t._payload._floating; // FIXME: approxEquals? 1250 case Type.String: 1251 return _payload._string == t._payload._string; 1252 case Type.Function: 1253 return _payload._function is t._payload._function; 1254 case Type.Array: 1255 return _payload._array == t._payload._array; 1256 } 1257 assert(0); 1258 } 1259 1260 public enum Type { 1261 Object, Array, Integral, Floating, String, Function, Boolean 1262 } 1263 1264 public Type payloadType() { 1265 return _type; 1266 } 1267 1268 private Type _type; 1269 1270 private union Payload { 1271 PrototypeObject _object; 1272 var[] _array; 1273 long _integral; 1274 double _floating; 1275 string _string; 1276 bool _boolean; 1277 var delegate(var _this, var[] args) _function; 1278 } 1279 1280 package VarMetadata _metadata; 1281 1282 public void _function(var delegate(var, var[]) f) { 1283 this._payload._function = f; 1284 this._type = Type.Function; 1285 } 1286 1287 /* 1288 public void _function(var function(var, var[]) f) { 1289 var delegate(var, var[]) dg; 1290 dg.ptr = null; 1291 dg.funcptr = f; 1292 this._function = dg; 1293 } 1294 */ 1295 1296 public void _object(PrototypeObject obj) { 1297 this._type = Type.Object; 1298 this._payload._object = obj; 1299 } 1300 1301 public PrototypeObject _object() { 1302 if(this._type == Type.Object) 1303 return this._payload._object; 1304 return null; 1305 } 1306 1307 package Payload _payload; 1308 1309 private void _requireType(Type t, string file = __FILE__, size_t line = __LINE__){ 1310 if(this.payloadType() != t) 1311 throw new DynamicTypeException(this, t, file, line); 1312 } 1313 1314 public var opSlice(var e1, var e2) { 1315 return this.opSlice(e1.get!ptrdiff_t, e2.get!ptrdiff_t); 1316 } 1317 1318 public var opSlice(ptrdiff_t e1, ptrdiff_t e2) { 1319 if(this.payloadType() == Type.Array) { 1320 if(e1 > _payload._array.length) 1321 e1 = _payload._array.length; 1322 if(e2 > _payload._array.length) 1323 e2 = _payload._array.length; 1324 return var(_payload._array[e1 .. e2]); 1325 } 1326 if(this.payloadType() == Type.String) { 1327 if(e1 > _payload._string.length) 1328 e1 = _payload._string.length; 1329 if(e2 > _payload._string.length) 1330 e2 = _payload._string.length; 1331 return var(_payload._string[e1 .. e2]); 1332 } 1333 if(this.payloadType() == Type.Object) { 1334 var operator = this["opSlice"]; 1335 if(operator._type == Type.Function) { 1336 return operator.call(this, e1, e2); 1337 } 1338 } 1339 1340 // might be worth throwing here too 1341 return var(null); 1342 } 1343 1344 /// Forwards to [opIndex] 1345 public @property ref var opDispatch(string name, string file = __FILE__, size_t line = __LINE__)() { 1346 return this[name]; 1347 } 1348 1349 /// Forwards to [opIndexAssign] 1350 public @property ref var opDispatch(string name, string file = __FILE__, size_t line = __LINE__, T)(T r) { 1351 return this.opIndexAssign!T(r, name); 1352 } 1353 1354 /// Looks up a sub-property of the object 1355 public ref var opIndex(var name, string file = __FILE__, size_t line = __LINE__) { 1356 return opIndex(name.get!string, file, line); 1357 } 1358 1359 /// Sets a sub-property of the object 1360 public ref var opIndexAssign(T)(T t, var name, string file = __FILE__, size_t line = __LINE__) { 1361 return opIndexAssign(t, name.get!string, file, line); 1362 } 1363 1364 public ref var opIndex(string name, string file = __FILE__, size_t line = __LINE__) { 1365 // if name is numeric, we should convert to int for arrays 1366 if(name.length && name[0] >= '0' && name[0] <= '9' && this.payloadType() == Type.Array) 1367 return opIndex(to!size_t(name), file, line); 1368 1369 if(this.payloadType() != Type.Object && name == "prototype") 1370 return prototype(); 1371 1372 if(name == "typeof") { 1373 var* tmp = new var; 1374 *tmp = to!string(this.payloadType()); 1375 return *tmp; 1376 } 1377 1378 if(name == "toJson") { 1379 var* tmp = new var; 1380 *tmp = to!string(this.toJson()); 1381 return *tmp; 1382 } 1383 1384 if(name == "length" && this.payloadType() == Type.String) { 1385 var* tmp = new var; 1386 *tmp = _payload._string.length; 1387 return *tmp; 1388 } 1389 if(name == "length" && this.payloadType() == Type.Array) { 1390 var* tmp = new var; 1391 *tmp = _payload._array.length; 1392 return *tmp; 1393 } 1394 if(name == "__prop" && this.payloadType() == Type.Object) { 1395 var* tmp = new var; 1396 (*tmp)._function = delegate var(var _this, var[] args) { 1397 if(args.length == 0) 1398 return var(null); 1399 if(args.length == 1) { 1400 auto peek = this._payload._object._peekMember(args[0].get!string, false); 1401 if(peek is null) 1402 return var(null); 1403 else 1404 return *peek; 1405 } 1406 if(args.length == 2) { 1407 auto peek = this._payload._object._peekMember(args[0].get!string, false); 1408 if(peek is null) { 1409 this._payload._object._properties[args[0].get!string] = args[1]; 1410 return var(null); 1411 } else { 1412 *peek = args[1]; 1413 return *peek; 1414 } 1415 1416 } 1417 throw new Exception("too many args"); 1418 }; 1419 return *tmp; 1420 } 1421 1422 PrototypeObject from; 1423 if(this.payloadType() == Type.Object) 1424 from = _payload._object; 1425 else { 1426 var pt = this.prototype(); 1427 assert(pt.payloadType() == Type.Object); 1428 from = pt._payload._object; 1429 } 1430 1431 if(from is null) { 1432 if(jsvar_throw) 1433 throw new DynamicTypeException(var(null), Type.Object, file, line); 1434 else 1435 return *(new var); 1436 } 1437 return from._getMember(name, true, false, false, file, line); 1438 } 1439 1440 public ref var opIndexAssign(T)(T t, string name, string file = __FILE__, size_t line = __LINE__) { 1441 if(this.payloadType == Type.Array && name.appearsNumeric()) { 1442 try { 1443 auto i = to!size_t(name); 1444 return opIndexAssign(t, i, file, line); 1445 } catch(Exception) 1446 {} // ignore bad index, use it as a string instead lol 1447 } 1448 _requireType(Type.Object); // FIXME? 1449 if(_payload._object is null) 1450 throw new DynamicTypeException(var(null), Type.Object, file, line); 1451 1452 return this._payload._object._setMember(name, var(t), false, false, false, file, line); 1453 } 1454 1455 public ref var opIndexAssignNoOverload(T)(T t, string name, string file = __FILE__, size_t line = __LINE__) { 1456 if(name.length && name[0] >= '0' && name[0] <= '9') 1457 return opIndexAssign(t, to!size_t(name), file, line); 1458 _requireType(Type.Object); // FIXME? 1459 if(_payload._object is null) 1460 throw new DynamicTypeException(var(null), Type.Object, file, line); 1461 1462 return this._payload._object._setMember(name, var(t), false, false, true, file, line); 1463 } 1464 1465 1466 public ref var opIndex(size_t idx, string file = __FILE__, size_t line = __LINE__) { 1467 if(_type == Type.Array) { 1468 auto arr = this._payload._array; 1469 if(idx < arr.length) 1470 return arr[idx]; 1471 } else if(_type == Type.Object) { 1472 // objects might overload opIndex 1473 var* n = new var(); 1474 if("opIndex" in this) 1475 *n = this["opIndex"](idx); 1476 return *n; 1477 } 1478 if(jsvar_throw) { 1479 throw new DynamicTypeException(this, Type.Array, file, line); 1480 } else { 1481 var* n = new var(); 1482 return *n; 1483 } 1484 } 1485 1486 public ref var opIndexAssign(T)(T t, size_t idx, string file = __FILE__, size_t line = __LINE__) { 1487 if(_type == Type.Array) { 1488 if(idx >= this._payload._array.length) 1489 this._payload._array.length = idx + 1; 1490 this._payload._array[idx] = t; 1491 return this._payload._array[idx]; 1492 } else if(_type == Type.Object) { 1493 return opIndexAssign(t, to!string(idx), file, line); 1494 } 1495 if(jsvar_throw) { 1496 throw new DynamicTypeException(this, Type.Array, file, line); 1497 } else { 1498 var* n = new var(); 1499 return *n; 1500 } 1501 } 1502 1503 ref var _getOwnProperty(string name, string file = __FILE__, size_t line = __LINE__) { 1504 if(_type == Type.Object) { 1505 if(_payload._object !is null) { 1506 auto peek = this._payload._object._peekMember(name, false); 1507 if(peek !is null) 1508 return *peek; 1509 } 1510 } 1511 //if(jsvar_throw) 1512 //throw new DynamicTypeException(this, Type.Object, file, line); 1513 var* n = new var(); 1514 return *n; 1515 } 1516 1517 @property static var emptyObject(PrototypeObject prototype = null) { 1518 var v; 1519 v._type = Type.Object; 1520 v._payload._object = new PrototypeObject(); 1521 v._payload._object.prototype = prototype; 1522 return v; 1523 } 1524 1525 /// 1526 @property static var emptyObject(var prototype) { 1527 if(prototype._type == Type.Object) 1528 return var.emptyObject(prototype._payload._object); 1529 return var.emptyObject(); 1530 } 1531 1532 @property PrototypeObject prototypeObject() { 1533 var v = prototype(); 1534 if(v._type == Type.Object) 1535 return v._payload._object; 1536 return null; 1537 } 1538 1539 // what I call prototype is more like what Mozilla calls __proto__, but tbh I think this is better so meh 1540 @property ref var prototype() { 1541 static var _arrayPrototype; 1542 static var _functionPrototype; 1543 static var _stringPrototype; 1544 1545 final switch(payloadType()) { 1546 case Type.Array: 1547 assert(_arrayPrototype._type == Type.Object); 1548 if(_arrayPrototype._payload._object is null) { 1549 _arrayPrototype._object = new PrototypeObject(); 1550 } 1551 1552 return _arrayPrototype; 1553 case Type.Function: 1554 assert(_functionPrototype._type == Type.Object); 1555 if(_functionPrototype._payload._object is null) { 1556 _functionPrototype._object = new PrototypeObject(); 1557 } 1558 1559 return _functionPrototype; 1560 case Type.String: 1561 assert(_stringPrototype._type == Type.Object); 1562 if(_stringPrototype._payload._object is null) { 1563 auto p = new PrototypeObject(); 1564 _stringPrototype._object = p; 1565 1566 var replaceFunction; 1567 replaceFunction._type = Type.Function; 1568 replaceFunction._function = (var _this, var[] args) { 1569 string s = _this.toString(); 1570 import std.array : replace; 1571 return var(std.array.replace(s, 1572 args[0].toString(), 1573 args[1].toString())); 1574 }; 1575 1576 p._properties["replace"] = replaceFunction; 1577 } 1578 1579 return _stringPrototype; 1580 case Type.Object: 1581 if(_payload._object) 1582 return _payload._object._prototype; 1583 // FIXME: should we do a generic object prototype? 1584 break; 1585 case Type.Integral: 1586 case Type.Floating: 1587 case Type.Boolean: 1588 // these types don't have prototypes 1589 } 1590 1591 1592 var* v = new var(null); 1593 return *v; 1594 } 1595 1596 /// 1597 @property static var emptyArray() { 1598 var v; 1599 v._type = Type.Array; 1600 return v; 1601 } 1602 1603 /// 1604 static var fromJson(const(char)[] json) { 1605 auto decoded = parseJSON(json); 1606 return var.fromJsonValue(decoded); 1607 } 1608 1609 /// 1610 static var fromJsonFile(const(char)[] filename) { 1611 import std.file; 1612 return var.fromJson(readText(filename)); 1613 } 1614 1615 /// 1616 static var fromJsonValue(JSONValue v) { 1617 var ret; 1618 1619 final switch(v.type) { 1620 case JSONType..string: 1621 ret = v.str; 1622 break; 1623 case JSONType.uinteger: 1624 ret = v.uinteger; 1625 break; 1626 case JSONType.integer: 1627 ret = v.integer; 1628 break; 1629 case JSONType.float_: 1630 ret = v.floating; 1631 break; 1632 case JSONType.object: 1633 ret = var.emptyObject; 1634 foreach(k, val; v.object) { 1635 ret[k] = var.fromJsonValue(val); 1636 } 1637 break; 1638 case JSONType.array: 1639 ret = var.emptyArray; 1640 ret._payload._array.length = v.array.length; 1641 foreach(idx, item; v.array) { 1642 ret._payload._array[idx] = var.fromJsonValue(item); 1643 } 1644 break; 1645 case JSONType.true_: 1646 ret = true; 1647 break; 1648 case JSONType.false_: 1649 ret = false; 1650 break; 1651 case JSONType.null_: 1652 ret = null; 1653 break; 1654 } 1655 1656 return ret; 1657 } 1658 1659 /// 1660 string toJson() { 1661 auto v = toJsonValue(); 1662 return toJSON(v); 1663 } 1664 1665 /// 1666 JSONValue toJsonValue() { 1667 JSONValue val; 1668 final switch(payloadType()) { 1669 case Type.Boolean: 1670 version(new_std_json) 1671 val = this._payload._boolean; 1672 else { 1673 if(this._payload._boolean) 1674 val.type = JSONType.true_; 1675 else 1676 val.type = JSONType.false_; 1677 } 1678 break; 1679 case Type.Object: 1680 version(new_std_json) { 1681 if(_payload._object is null) { 1682 val = null; 1683 } else { 1684 val = _payload._object.toJsonValue(); 1685 } 1686 } else { 1687 if(_payload._object is null) { 1688 val.type = JSONType.null_; 1689 } else { 1690 val.type = JSONType.object; 1691 foreach(k, v; _payload._object._properties) { 1692 val.object[k] = v.toJsonValue(); 1693 } 1694 } 1695 } 1696 break; 1697 case Type.String: 1698 version(new_std_json) { } else { 1699 val.type = JSONType..string; 1700 } 1701 val.str = _payload._string; 1702 break; 1703 case Type.Integral: 1704 version(new_std_json) { } else { 1705 val.type = JSONType.integer; 1706 } 1707 val.integer = _payload._integral; 1708 break; 1709 case Type.Floating: 1710 version(new_std_json) { } else { 1711 val.type = JSONType.float_; 1712 } 1713 val.floating = _payload._floating; 1714 break; 1715 case Type.Array: 1716 auto a = _payload._array; 1717 JSONValue[] tmp; 1718 tmp.length = a.length; 1719 foreach(i, v; a) { 1720 tmp[i] = v.toJsonValue(); 1721 } 1722 1723 version(new_std_json) { 1724 val = tmp; 1725 } else { 1726 val.type = JSONType.array; 1727 val.array = tmp; 1728 } 1729 break; 1730 case Type.Function: 1731 version(new_std_json) 1732 val = null; 1733 else 1734 val.type = JSONType.null_; // ideally we would just skip it entirely... 1735 break; 1736 } 1737 return val; 1738 } 1739 } 1740 1741 class PrototypeObject { 1742 string name; 1743 var _prototype; 1744 1745 package PrototypeObject _secondary; // HACK don't use this 1746 1747 PrototypeObject prototype() { 1748 if(_prototype.payloadType() == var.Type.Object) 1749 return _prototype._payload._object; 1750 return null; 1751 } 1752 1753 PrototypeObject prototype(PrototypeObject set) { 1754 this._prototype._object = set; 1755 return set; 1756 } 1757 1758 override string toString() { 1759 1760 var* ts = _peekMember("toString", true); 1761 if(ts) { 1762 var _this; 1763 _this._object = this; 1764 return (*ts).call(_this).get!string; 1765 } 1766 1767 JSONValue val; 1768 version(new_std_json) { 1769 JSONValue[string] tmp; 1770 foreach(k, v; this._properties) 1771 tmp[k] = v.toJsonValue(); 1772 val.object = tmp; 1773 } else { 1774 val.type = JSONType.object; 1775 foreach(k, v; this._properties) 1776 val.object[k] = v.toJsonValue(); 1777 } 1778 1779 return toJSON(val); 1780 } 1781 1782 var[string] _properties; 1783 1784 PrototypeObject copy() { 1785 auto n = new PrototypeObject(); 1786 n.prototype = this.prototype; 1787 n.name = this.name; 1788 foreach(k, v; _properties) { 1789 n._properties[k] = v._copy; 1790 } 1791 return n; 1792 } 1793 1794 bool isSpecial() { return false; } 1795 1796 PrototypeObject new_(PrototypeObject newThis) { 1797 // if any of the prototypes are D objects, we need to try to copy them. 1798 auto p = prototype; 1799 1800 PrototypeObject[32] stack; 1801 PrototypeObject[] fullStack = stack[]; 1802 int stackPos; 1803 1804 while(p !is null) { 1805 1806 if(p.isSpecial()) { 1807 auto n = new PrototypeObject(); 1808 1809 auto proto = p.new_(n); 1810 1811 while(stackPos) { 1812 stackPos--; 1813 auto pr = fullStack[stackPos].copy(); 1814 pr.prototype = proto; 1815 proto = pr; 1816 } 1817 1818 n.prototype = proto; 1819 n.name = this.name; 1820 foreach(k, v; _properties) { 1821 n._properties[k] = v._copy; 1822 } 1823 1824 return n; 1825 } 1826 1827 if(stackPos >= fullStack.length) 1828 fullStack ~= p; 1829 else 1830 fullStack[stackPos] = p; 1831 stackPos++; 1832 1833 p = p.prototype; 1834 } 1835 1836 return copy(); 1837 } 1838 1839 PrototypeObject copyPropertiesFrom(PrototypeObject p) { 1840 foreach(k, v; p._properties) { 1841 this._properties[k] = v._copy; 1842 } 1843 return this; 1844 } 1845 1846 var* _peekMember(string name, bool recurse) { 1847 if(name == "prototype") 1848 return &_prototype; 1849 1850 auto curr = this; 1851 1852 // for the secondary hack 1853 bool triedOne = false; 1854 // for the secondary hack 1855 PrototypeObject possibleSecondary; 1856 1857 tryAgain: 1858 do { 1859 auto prop = name in curr._properties; 1860 if(prop is null) { 1861 // the secondary hack is to do more scoping in the script, it is really hackish 1862 if(possibleSecondary is null) 1863 possibleSecondary = curr._secondary; 1864 1865 if(!recurse) 1866 break; 1867 else 1868 curr = curr.prototype; 1869 } else 1870 return prop; 1871 } while(curr); 1872 1873 if(possibleSecondary !is null) { 1874 curr = possibleSecondary; 1875 if(!triedOne) { 1876 triedOne = true; 1877 goto tryAgain; 1878 } 1879 } 1880 1881 return null; 1882 } 1883 1884 // FIXME: maybe throw something else 1885 /*package*/ ref var _getMember(string name, bool recurse, bool throwOnFailure, bool returnRawProperty = false, string file = __FILE__, size_t line = __LINE__) { 1886 var* mem = _peekMember(name, recurse); 1887 1888 if(mem !is null) { 1889 // If it is a property, we need to call the getter on it 1890 if((*mem).payloadType == var.Type.Object && cast(PropertyPrototype) (*mem)._payload._object) { 1891 if(returnRawProperty) 1892 return *mem; 1893 auto prop = cast(PropertyPrototype) (*mem)._payload._object; 1894 return prop.get; 1895 } 1896 return *mem; 1897 } 1898 1899 mem = _peekMember("opIndex", recurse); 1900 if(mem !is null) { 1901 auto n = new var; 1902 *n = ((*mem)(name)); 1903 return *n; 1904 } 1905 1906 // if we're here, the property was not found, so let's implicitly create it 1907 if(throwOnFailure) 1908 throw new DynamicTypeException("no such property " ~ name, file, line); 1909 var n; 1910 this._properties[name] = n; 1911 return this._properties[name]; 1912 } 1913 1914 // FIXME: maybe throw something else 1915 /*package*/ ref var _setMember(string name, var t, bool recurse, bool throwOnFailure, bool suppressOverloading, string file = __FILE__, size_t line = __LINE__) { 1916 var* mem = _peekMember(name, recurse); 1917 1918 bool onParent = false; 1919 1920 if(mem is null && !recurse) { 1921 mem = _peekMember(name, true); // properties need the check anyway as a setter might be on a prototype 1922 onParent = true; 1923 } 1924 1925 if(mem !is null) { 1926 // Property check - the setter should be proxied over to it 1927 if((*mem).payloadType == var.Type.Object && cast(PropertyPrototype) (*mem)._payload._object) { 1928 auto prop = cast(PropertyPrototype) (*mem)._payload._object; 1929 return prop.set(t); 1930 } 1931 if(!onParent) { 1932 *mem = t; 1933 return *mem; 1934 } else { 1935 mem = null; 1936 } 1937 } 1938 1939 if(!suppressOverloading) { 1940 mem = _peekMember("opIndexAssign", true); 1941 if(mem !is null) { 1942 auto n = new var; 1943 *n = ((*mem)(t, name)); 1944 return *n; 1945 } 1946 } 1947 1948 // if we're here, the property was not found, so let's implicitly create it 1949 if(throwOnFailure) 1950 throw new DynamicTypeException("no such property " ~ name, file, line); 1951 this._properties[name] = t; 1952 return this._properties[name]; 1953 } 1954 1955 JSONValue toJsonValue() { 1956 JSONValue val; 1957 JSONValue[string] tmp; 1958 foreach(k, v; this._properties) { 1959 // if it is an overload set and/or a function, just skip it. 1960 // or really if it is a wrapped native object it should prolly just be skipped anyway 1961 // unless it actually defines a toJson. 1962 if(v.payloadType == var.Type.Function) 1963 continue; 1964 if(v.payloadType == var.Type.Object) { 1965 // I'd love to get the json value out but idk. FIXME 1966 if(v._payload._object is null) continue; 1967 if(auto wno = cast(WrappedNativeObject) v._payload._object) { 1968 auto obj = wno.getObject(); 1969 if(obj is null) 1970 tmp[k] = null; 1971 else 1972 tmp[k] = obj.toString(); 1973 continue; 1974 } else if(typeid(PrototypeObject) !is typeid(v._payload._object)) 1975 continue; 1976 } 1977 1978 tmp[k] = v.toJsonValue(); 1979 } 1980 1981 val = tmp; 1982 return val; 1983 } 1984 1985 public int opApply(scope int delegate(var, ref var) dg) { 1986 foreach(k, v; this._properties) { 1987 if(v.payloadType == var.Type.Object && cast(PropertyPrototype) v._payload._object) 1988 v = (cast(PropertyPrototype) v._payload._object).get; 1989 if(auto result = dg(var(k), v)) 1990 return result; 1991 } 1992 return 0; 1993 } 1994 } 1995 1996 // A property is a special type of object that can only be set by assigning 1997 // one of these instances to foo.child._object. When foo.child is accessed and it 1998 // is an instance of PropertyPrototype, it will return the getter. When foo.child is 1999 // set (excluding direct assignments through _type), it will call the setter. 2000 class PropertyPrototype : PrototypeObject { 2001 var delegate() getter; 2002 void delegate(var) setter; 2003 this(var delegate() getter, void delegate(var) setter) { 2004 this.getter = getter; 2005 this.setter = setter; 2006 } 2007 2008 override string toString() { 2009 return get.toString(); 2010 } 2011 2012 ref var get() { 2013 var* g = new var(); 2014 *g = getter(); 2015 return *g; 2016 } 2017 2018 ref var set(var t) { 2019 setter(t); 2020 return get; 2021 } 2022 2023 override JSONValue toJsonValue() { 2024 return get.toJsonValue(); 2025 } 2026 } 2027 2028 /// 2029 struct ScriptLocation { 2030 string scriptFilename; /// 2031 int lineNumber; /// 2032 } 2033 2034 class DynamicTypeException : Exception { 2035 this(string msg, string file = __FILE__, size_t line = __LINE__) { 2036 super(msg, file, line); 2037 } 2038 this(var v, var.Type required, string file = __FILE__, size_t line = __LINE__) { 2039 import std.string; 2040 if(v.payloadType() == required) 2041 super(format("Tried to use null as a %s", required), file, line); 2042 else { 2043 super(format("Tried to use %s%s as a %s", v == null ? "null ": "", v.payloadType(), required), file, line); 2044 } 2045 } 2046 2047 override void toString(scope void delegate(in char[]) sink) const { 2048 import std.format; 2049 if(varName.length) 2050 sink(varName); 2051 if(callStack.length) { 2052 sink("arsd.jsvar.DynamicTypeException@"); 2053 sink(file); 2054 sink("("); 2055 sink(to!string(line)); 2056 sink("): "); 2057 sink(msg); 2058 foreach(cs; callStack) 2059 sink(format("\n\t%s:%s", cs.scriptFilename, cs.lineNumber)); 2060 2061 bool found; 2062 void hiddenSink(in char[] str) { 2063 // I just want to hide the call stack until the interpret call... 2064 // since the script stack above is more meaningful to users. 2065 // 2066 // but then I will go back to the D functions once on the outside. 2067 import std.string; 2068 if(found) 2069 sink(str); 2070 else if(str.indexOf("arsd.script.interpret(") != -1) 2071 found = true; 2072 } 2073 2074 sink("\n--------"); 2075 2076 super.toString(&hiddenSink); 2077 } else { 2078 super.toString(sink); 2079 } 2080 } 2081 2082 ScriptLocation[] callStack; 2083 string varName; 2084 } 2085 2086 template makeAscii() { 2087 string helper() { 2088 string s; 2089 foreach(i; 0 .. 128) 2090 s ~= cast(char) i; 2091 return s; 2092 } 2093 2094 enum makeAscii = helper(); 2095 } 2096 2097 package interface VarMetadata { } 2098 2099 interface ScriptableSubclass { 2100 void setScriptVar(var); 2101 var getScriptVar(); 2102 final bool methodOverriddenByScript(string name) { 2103 PrototypeObject t = getScriptVar().get!PrototypeObject; 2104 // the top one is the native object from subclassable so we don't want to go all the way there to avoid endless recursion 2105 //import std.stdio; writeln("checking ", name , " ...", "wtf"); 2106 if(t !is null) 2107 while(!t.isSpecial) { 2108 if(t._peekMember(name, false) !is null) 2109 return true; 2110 t = t.prototype; 2111 } 2112 return false; 2113 } 2114 } 2115 2116 /++ 2117 EXPERIMENTAL 2118 2119 Allows you to make a class available to the script rather than just class objects. 2120 You can subclass it in script and then call the methods again through the original 2121 D interface. With caveats... 2122 2123 2124 Assumes ALL $(I virtual) methods and constructors are scriptable, but requires 2125 `@scriptable` to be present on final or static methods. This may change in the future. 2126 2127 Note that it has zero support for `@safe`, `pure`, `nothrow`, and other attributes 2128 at this time and will skip that use those. I may be able to loosen this in the 2129 future as well but I have no concrete plan to at this time. You can still mark 2130 them as `@scriptable` to call them from the script, but they can never be overridden 2131 by script code because it cannot verify those guarantees hold true. 2132 2133 Ditto on `const` and `immutable`. 2134 2135 Its behavior on overloads is currently undefined - it may keep only any random 2136 overload as the only one and do dynamic type conversions to cram data into it. 2137 This is likely to change in the future but for now try not to use this on classes 2138 with overloaded methods. 2139 2140 It also does not wrap member variables unless explicitly marked `@scriptable`; it 2141 is meant to communicate via methods. 2142 2143 History: 2144 Added April 25, 2020 2145 +/ 2146 static if(__traits(compiles, mixin(q{ () { static foreach(i; [1,2]) {} } }) )) 2147 mixin(q{ 2148 var subclassable(T)() if(is(T == class) || is(T == interface)) { 2149 import std.traits; 2150 2151 static final class ScriptableT : T, ScriptableSubclass { 2152 var _this; 2153 void setScriptVar(var v) { _this = v; } 2154 var getScriptVar() { return _this; } 2155 bool _next_devirtualized; 2156 2157 // @scriptable size_t _nativeHandle_() { return cast(size_t) cast(void*) this;} 2158 2159 static if(__traits(compiles, __traits(getOverloads, T, "__ctor"))) 2160 static foreach(ctor; __traits(getOverloads, T, "__ctor")) 2161 @scriptable this(Parameters!ctor p) { super(p); } 2162 2163 static foreach(memberName; __traits(allMembers, T)) { 2164 static if(memberName != "toHash") 2165 static foreach(overload; __traits(getOverloads, T, memberName)) 2166 static if(__traits(isVirtualMethod, overload)) 2167 static if(!__traits(isFinalFunction, overload)) 2168 static if(!__traits(isDeprecated, overload)) 2169 // note: overload behavior undefined 2170 static if(!(functionAttributes!(overload) & (FunctionAttribute.pure_ | FunctionAttribute.safe | FunctionAttribute.trusted | FunctionAttribute.nothrow_ | FunctionAttribute.const_ | FunctionAttribute.inout_))) 2171 static if(!hasRefParam!overload) 2172 static if(__traits(getFunctionVariadicStyle, overload) == "none") 2173 static if(__traits(identifier, overload) == memberName) // to filter out aliases 2174 mixin(q{ 2175 @scriptable 2176 override ReturnType!(overload) 2177 }~memberName~q{ 2178 (Parameters!(overload) p) 2179 { 2180 //import std.stdio; writeln("calling ", T.stringof, ".", memberName, " - ", methodOverriddenByScript(memberName), "/", _next_devirtualized, " on ", cast(size_t) cast(void*) this); 2181 if(_next_devirtualized || !methodOverriddenByScript(memberName)) { 2182 _next_devirtualized = false; 2183 return __traits(getMember, super, memberName)(p); 2184 } 2185 return _this[memberName].call(_this, p).get!(typeof(return)); 2186 } 2187 }); 2188 } 2189 2190 // I don't want to necessarily call a constructor but I need an object t use as the prototype 2191 // hence this faked one. hopefully the new operator will see void[] and assume it can have GC ptrs... 2192 static ScriptableT _allocate_(PrototypeObject newThis) { 2193 void[] store = new void[](__traits(classInstanceSize, ScriptableT)); 2194 store[] = typeid(ScriptableT).initializer[]; 2195 ScriptableT dummy = cast(ScriptableT) store.ptr; 2196 dummy._this = var(newThis); 2197 //import std.stdio; writeln("Allocating new ", cast(ulong) store.ptr); 2198 return dummy; 2199 } 2200 } 2201 2202 ScriptableT dummy = ScriptableT._allocate_(null); 2203 2204 var proto = wrapNativeObject!(ScriptableT, true)(dummy); 2205 2206 var f = var.emptyObject; 2207 f.prototype = proto; 2208 2209 return f; 2210 } 2211 2212 template hasRefParam(alias overload) { 2213 bool helper() { 2214 static if(is(typeof(overload) P == __parameters)) 2215 foreach(idx, p; P) 2216 foreach(thing; __traits(getParameterStorageClasses, overload, idx)) 2217 if(thing == "ref") 2218 return true; 2219 return false; 2220 } 2221 2222 enum hasRefParam = helper(); 2223 } 2224 2225 }); 2226 2227 /// Demonstrates tested capabilities of [subclassable] 2228 version(with_arsd_script) 2229 unittest { 2230 interface IFoo { 2231 string method(); 2232 int method2(); 2233 int args(int, int); 2234 } 2235 // note the static is just here because this 2236 // is written in a unittest; it shouldn't actually 2237 // be necessary under normal circumstances. 2238 static class Foo : IFoo { 2239 ulong handle() { return cast(ulong) cast(void*) this; } 2240 string method() { return "Foo"; } 2241 int method2() { return 10; } 2242 int args(int a, int b) { 2243 //import std.stdio; writeln(a, " + ", b, " + ", member_, " on ", cast(ulong) cast(void*) this); 2244 return member_+a+b; } 2245 2246 int member_; 2247 @property int member(int i) { return member_ = i; } 2248 @property int member() { return member_; } 2249 2250 @scriptable final int fm() { return 56; } 2251 } 2252 static class Bar : Foo { 2253 override string method() { return "Bar"; } 2254 string test1() { return test2(); } 2255 string test2() { return "test2"; } 2256 2257 int acceptFoo(Foo f) { 2258 return f.method2(); 2259 } 2260 } 2261 static class Baz : Bar { 2262 override int method2() { return 20; } 2263 } 2264 2265 static class WithCtor { 2266 // constructors work but are iffy with overloads.... 2267 this(int arg) { this.arg = arg; } 2268 @scriptable int arg; // this is accessible cuz it is @scriptable 2269 int getValue() { return arg; } 2270 } 2271 2272 var globals = var.emptyObject; 2273 globals.Foo = subclassable!Foo; 2274 globals.Bar = subclassable!Bar; 2275 globals.Baz = subclassable!Baz; 2276 globals.WithCtor = subclassable!WithCtor; 2277 2278 import arsd.script; 2279 2280 interpret(q{ 2281 // can instantiate D classes added via subclassable 2282 var foo = new Foo(); 2283 // and call its methods... 2284 assert(foo.method() == "Foo"); 2285 assert(foo.method2() == 10); 2286 2287 foo.member(55); 2288 2289 // proves the new operator actually creates new D 2290 // objects as well to avoid sharing instance state. 2291 var foo2 = new Foo(); 2292 assert(foo2.handle() != foo.handle()); 2293 2294 // passing arguments works 2295 assert(foo.args(2, 4) == 6 + 55); // (and sanity checks operator precedence) 2296 2297 var bar = new Bar(); 2298 assert(bar.method() == "Bar"); 2299 assert(bar.method2() == 10); 2300 2301 assert(bar.acceptFoo(new Foo()) == 10); 2302 assert(bar.acceptFoo(new Baz()) == 20); 2303 2304 // this final member is accessible because it was marked @scriptable 2305 assert(bar.fm() == 56); 2306 2307 assert(bar.test1() == "test2"); 2308 assert(bar.test2() == "test2"); 2309 2310 // the script can even subclass D classes! 2311 class Amazing : Bar { 2312 // and override its methods 2313 var inst = 99; 2314 function method() { 2315 return "Amazing"; 2316 } 2317 2318 // note: to access instance members or virtual call lookup you MUST use the `this` keyword 2319 // otherwise the function will be called with scope limited to this class itself (similar to javascript) 2320 function other() { 2321 // this.inst is needed to get the instance variable (otherwise it would only look for a static var) 2322 // and this.method triggers dynamic lookup there, so it will get children's overridden methods if there is one 2323 return this.inst ~ this.method(); 2324 } 2325 2326 function args(a, b) { 2327 // calling parent class method still possible 2328 return super.args(a*2, b*2); 2329 } 2330 2331 function test2() { 2332 return "script test"; 2333 } 2334 } 2335 2336 var amazing = new Amazing(); 2337 assert(amazing.method() == "Amazing"); 2338 assert(amazing.method2() == 10); // calls back to the parent class 2339 amazing.member(5); 2340 2341 assert(amazing.test1() == "script test"); // even the virtual method call from D goes into the script override 2342 2343 // this line I can paste down to interactively debug the test btw. 2344 //}, globals); repl!true(globals); interpret(q{ 2345 2346 assert(amazing.args(2, 4) == 12+5); 2347 2348 var wc = new WithCtor(5); // argument passed to constructor 2349 assert(wc.getValue() == 5); 2350 2351 // confirm the property read works too 2352 assert(wc.arg == 5); 2353 2354 // but property WRITING is currently not working though. 2355 2356 2357 class DoubleChild : Amazing { 2358 function method() { 2359 return "DoubleChild"; 2360 } 2361 } 2362 2363 // can also do a child of a child class 2364 var dc = new DoubleChild(); 2365 assert(dc.method() == "DoubleChild"); 2366 assert(dc.other() == "99DoubleChild"); // the `this.method` means it uses the replacement now 2367 assert(dc.method2() == 10); // back to the D grandparent 2368 assert(dc.args(2, 4) == 12); // but the args impl from above 2369 }, globals); 2370 2371 Foo foo = globals.foo.get!Foo; // get the native object back out 2372 assert(foo.member == 55); // and see mutation via properties proving object mutability 2373 assert(globals.foo.get!Bar is null); // cannot get the wrong class out of it 2374 assert(globals.foo.get!Object !is null); // but can do parent classes / interfaces 2375 assert(globals.foo.get!IFoo !is null); 2376 assert(globals.bar.get!Foo !is null); // the Bar can also be a Foo 2377 2378 Bar amazing = globals.amazing.get!Bar; // instance of the script's class is still accessible through parent D class or interface 2379 assert(amazing !is null); // object exists 2380 assert(amazing.method() == "Amazing"); // calls the override from the script 2381 assert(amazing.method2() == 10); // non-overridden function works as expected 2382 2383 IFoo iamazing = globals.amazing.get!IFoo; // and through just the interface works the same way 2384 assert(iamazing !is null); 2385 assert(iamazing.method() == "Amazing"); 2386 assert(iamazing.method2() == 10); 2387 2388 // and here ensuring the interface we got out can go back in and back out too 2389 var globals2 = var.emptyObject; 2390 globals2.opDispatch!"iamazing2" = iamazing; 2391 // though inside, it is treated as an opaque reference since nothing is @scriptable 2392 // so nothing to test there... 2393 // however getting it back out should work fine 2394 IFoo gotten = globals2.iamazing2.get!IFoo; 2395 assert(gotten !is null); 2396 assert(gotten is iamazing); 2397 //import std.stdio; writeln("wtf", iamazing.method()); 2398 2399 // now this seems obvious, but adding the interface can actually overwrite the dynamic 2400 // prototype.... i think 2401 //assert(gotten.method() == "Amazing"); 2402 2403 //https://issues.dlang.org/show_bug.cgi?id=22011 necessitated the static! 2404 static class CFoo : IFoo { 2405 @scriptable string method() { return "CFoo"; } 2406 int method2() { return 55; } 2407 int args(int, int) { return 6 + 5; } 2408 } 2409 2410 globals.opDispatch!"iamazing3" = new CFoo; // and also just testing a standard class assign 2411 IFoo input = new CFoo(); 2412 globals.opDispatch!"iamazing4" = input; // and interface 2413 interpret(q{ 2414 var a = iamazing3.method(); 2415 assert(a == "CFoo"); // works cuz the class is scriptable 2416 // but the following line is NOT available because the interface itself is not marked scriptable 2417 //assert(iamazing4.method() == "CFoo"); 2418 }, globals); 2419 2420 // but we can get it right back out 2421 IFoo got2 = globals.iamazing4.get!IFoo; 2422 assert(got2 is input); 2423 assert(got2.method() == "CFoo"); 2424 } 2425 2426 unittest { 2427 static struct Foo { 2428 struct Bar { 2429 int[] x; 2430 } 2431 2432 Bar bar; 2433 } 2434 2435 assert( 2436 var.fromJson(`{ 2437 "bar":{ "x": [1,2,3]} 2438 }`).get!Foo 2439 == 2440 Foo(Foo.Bar([1,2,3])) 2441 ); 2442 } 2443 2444 version(with_arsd_script) 2445 unittest { 2446 import arsd.script; 2447 static class Test { 2448 @scriptable int a; 2449 } 2450 2451 Test test = new Test; 2452 2453 test.a = 15; 2454 2455 var globals = var.emptyObject; 2456 globals.test = test; 2457 globals.Test = subclassable!Test; 2458 2459 interpret(q{ 2460 test.a = 4; 2461 var wtf = test.a += 5; 2462 assert(wtf == 9); 2463 assert(test.a == 9); 2464 var test2 = new Test; 2465 test2.a = 2; 2466 test2.a += 5; 2467 assert(test2.a == 7); 2468 }, globals); 2469 2470 assert(test.a == 9); 2471 assert(globals.test2.get!Test.a == 7); 2472 } 2473 2474 // just a base class we can reference when looking for native objects 2475 class WrappedNativeObject : PrototypeObject { 2476 TypeInfo wrappedType; 2477 abstract Object getObject(); 2478 } 2479 2480 template helper(alias T) { alias helper = T; } 2481 2482 /++ 2483 Wraps a class. If you are manually managing the memory, remember the jsvar may keep a reference to the object; don't free it! 2484 2485 To use this: `var a = wrapNativeObject(your_d_object);` OR `var a = your_d_object`; 2486 2487 By default, it will wrap all methods and members with a public or greater protection level. The second template parameter can filter things differently. FIXME implement this 2488 2489 History: 2490 This became the default after April 24, 2020. Previously, [var.opAssign] would [wrapOpaquely] instead. 2491 +/ 2492 WrappedNativeObject wrapNativeObject(Class, bool special = false)(Class obj) if(is(Class == class) || is(Class == interface)) { 2493 import std.meta; 2494 static class WrappedNativeObjectImpl : WrappedNativeObject { 2495 override Object getObject() { 2496 return cast(Object) obj; 2497 } 2498 2499 override bool isSpecial() { return special; } 2500 2501 static if(special) 2502 override WrappedNativeObject new_(PrototypeObject newThis) { 2503 return new WrappedNativeObjectImpl(obj._allocate_(newThis)); 2504 } 2505 2506 Class obj; 2507 2508 this(Class objIn) { 2509 this.obj = objIn; 2510 wrappedType = typeid(cast(Object) obj); 2511 // wrap the other methods 2512 // and wrap members as scriptable properties 2513 2514 foreach(memberName; __traits(allMembers, Class)) static if(!mixin(deprecationCheck()) && is(typeof(__traits(getMember, obj, memberName)) type)) { 2515 static if(is(type == function)) { 2516 auto os = new OverloadSet(); 2517 foreach(idx, overload; AliasSeq!(__traits(getOverloads, obj, memberName))) static if(.isScriptable!(__traits(getAttributes, overload))()) { 2518 var gen; 2519 gen._function = delegate (var vthis_, var[] vargs) { 2520 Parameters!(__traits(getOverloads, Class, memberName)[idx]) fargs; 2521 2522 2523 enum lol = static_foreach(fargs.length, 1, -1, 2524 `__traits(getOverloads, obj, memberName)[idx](`,``,` < vargs.length ? vargs[`,`].get!(typeof(fargs[`,`])) : ParamDefault!(__traits(getOverloads, Class, memberName)[idx], `,`)(),`,`)`); 2525 /* 2526 enum lol = static_foreach(fargs.length, 1, -1, 2527 `__traits(getOverloads, obj, memberName)[idx](`,``,` < vargs.length ? vargs[`,`].get!(typeof(fargs[`,`])) : 2528 typeof(fargs[`,`]).init,`,`)`); 2529 */ 2530 2531 // FIXME: what if there are multiple @scriptable overloads?! 2532 // FIXME: what about @properties? 2533 2534 static if(special) { 2535 Class obj; 2536 //if(vthis_.payloadType() != var.Type.Object) { import std.stdio; writeln("getwno on ", vthis_); } 2537 // the native object might be a step or two up the prototype 2538 // chain due to script subclasses, need to find it... 2539 while(vthis_ != null) { 2540 obj = vthis_.getWno!Class; 2541 if(obj !is null) 2542 break; 2543 vthis_ = vthis_.prototype; 2544 } 2545 2546 if(obj is null) throw new Exception("null native object"); 2547 } 2548 2549 static if(special) { 2550 obj._next_devirtualized = true; 2551 scope(exit) obj._next_devirtualized = false; 2552 } 2553 2554 var ret; 2555 2556 static if(!is(typeof(__traits(getOverloads, obj, memberName)[idx](fargs)) == void)) 2557 ret = mixin(lol); 2558 else 2559 mixin(lol ~ ";"); 2560 2561 return ret; 2562 }; 2563 2564 Parameters!(overload) fargs; 2565 // FIXME: if an argument type is a class, we need to actually look it up in the script context somehow 2566 var[] definition; 2567 foreach(arg; fargs) { 2568 definition ~= var(arg); 2569 } 2570 2571 //import std.stdio; writeln(Class.stringof, ".", memberName, " ", definition); 2572 os.addOverload(OverloadSet.Overload(definition, gen)); 2573 } 2574 2575 _properties[memberName] = var(os); 2576 } else { 2577 alias fuckThisLanguage = __traits(getOverloads, Class, memberName, true); 2578 static if(fuckThisLanguage.length) 2579 alias fuckYourPointlessDeprecations = fuckThisLanguage[0]; 2580 else 2581 alias fuckYourPointlessDeprecations = __traits(getMember, Class, memberName); 2582 static if(.isScriptable!(__traits(getAttributes, fuckYourPointlessDeprecations))()) 2583 // if it has a type but is not a function, it is prolly a member 2584 _properties[memberName] = new PropertyPrototype( 2585 () => var(__traits(getMember, obj, memberName)), 2586 (var v) { 2587 // read-only property hack 2588 static if(__traits(compiles, __traits(getMember, obj, memberName) = v.get!(type))) 2589 __traits(getMember, obj, memberName) = v.get!(type); 2590 }); 2591 } 2592 } 2593 } 2594 } 2595 2596 return new WrappedNativeObjectImpl(obj); 2597 } 2598 2599 import std.traits; 2600 class WrappedOpaque(T) : PrototypeObject if(isPointer!T || is(T == class) || is(T == interface)) { 2601 T wrapped; 2602 this(T t) { 2603 wrapped = t; 2604 } 2605 T wrapping() { 2606 return wrapped; 2607 } 2608 } 2609 class WrappedOpaque(T) : PrototypeObject if(!isPointer!T && !is(T == class) && !is(T == interface)) { 2610 T* wrapped; 2611 this(T t) { 2612 wrapped = new T; 2613 (cast() *wrapped) = t; 2614 } 2615 this(T* t) { 2616 wrapped = t; 2617 } 2618 T* wrapping() { 2619 return wrapped; 2620 } 2621 } 2622 2623 WrappedOpaque!Obj wrapOpaquely(Obj)(Obj obj) { 2624 static if(is(Obj == class)) { 2625 if(obj is null) 2626 return null; 2627 } 2628 return new WrappedOpaque!Obj(obj); 2629 } 2630 2631 /** 2632 Wraps an opaque struct pointer in a module with ufcs functions 2633 */ 2634 WrappedNativeObject wrapUfcs(alias Module, Type)(Type obj) { 2635 import std.meta; 2636 return new class WrappedNativeObject { 2637 override Object getObject() { 2638 return null; // not actually an object! but close to 2639 } 2640 2641 this() { 2642 wrappedType = typeid(Type); 2643 // wrap the other methods 2644 // and wrap members as scriptable properties 2645 2646 foreach(memberName; __traits(allMembers, Module)) static if(is(typeof(__traits(getMember, Module, memberName)) type)) { 2647 static if(is(type == function)) { 2648 foreach(idx, overload; AliasSeq!(__traits(getOverloads, Module, memberName))) static if(.isScriptable!(__traits(getAttributes, __traits(getOverloads, Module, memberName)[idx]))()) { // lol i have to do the second getOverload to stop the compiler from deprecating 2649 auto helper = &__traits(getOverloads, Module, memberName)[idx]; 2650 static if(Parameters!helper.length >= 1 && is(Parameters!helper[0] == Type)) { 2651 // this staticMap is a bit of a hack so it can handle `in float`... liable to break with others, i'm sure 2652 _properties[memberName] = (staticMap!(Unqual, Parameters!helper[1 .. $]) args) { 2653 return __traits(getOverloads, Module, memberName)[idx](obj, args); 2654 }; 2655 } 2656 } 2657 } 2658 } 2659 } 2660 }; 2661 } 2662 2663 bool isCallableJsvarObject(var possibility) { 2664 if(possibility.payloadType == var.Type.Function) 2665 return true; 2666 if(possibility.payloadType == var.Type.Object) { 2667 if(possibility._payload._object is null) 2668 return false; 2669 2670 if(var* test = possibility._payload._object._peekMember("opCall", true)) { 2671 if(isCallableJsvarObject(*test)) 2672 return true; 2673 } 2674 2675 } 2676 return false; 2677 } 2678 2679 2680 bool isScriptable(attributes...)() { 2681 bool nonConstConditionForWorkingAroundASpuriousDmdWarning = true; 2682 foreach(attribute; attributes) { 2683 static if(is(typeof(attribute) == string)) { 2684 static if(attribute == scriptable) { 2685 if(nonConstConditionForWorkingAroundASpuriousDmdWarning) 2686 return true; 2687 } 2688 } 2689 } 2690 return false; 2691 } 2692 2693 bool isScriptableOpaque(T)() { 2694 static if(is(typeof(T.isOpaqueStruct) == bool)) 2695 return T.isOpaqueStruct == true; 2696 else 2697 return false; 2698 } 2699 2700 int typeCompatibilityScore(var arg, var type) { 2701 int thisScore = 0; 2702 2703 if(type.payloadType == var.Type.Object && type._payload._object is null) { 2704 thisScore += 1; // generic match 2705 return thisScore; 2706 } 2707 if(arg.payloadType == var.Type.Integral && type.payloadType == var.Type.Floating) { 2708 thisScore += 2; // match with implicit conversion 2709 // FIXME: want to support implicit from whatever to bool? 2710 } else if(arg.payloadType != type.payloadType) { 2711 thisScore = 0; 2712 return thisScore; 2713 } else { 2714 // exact type category match 2715 if(type.payloadType == var.Type.Array) { 2716 // arrays not really supported here.... 2717 // so just like if both are arrays i'll take 2718 // it as a bare minimum but i don't love it otherwise 2719 if(arg.payloadType == var.Type.Array) 2720 thisScore = 1; 2721 else 2722 thisScore = 0; 2723 return thisScore; 2724 } else if(type.payloadType == var.Type.Object) { 2725 // objects are the interesting one... 2726 // want to see if it matches by seeing if the 2727 // given type is identical or its prototype is one of the given type's prototype chain 2728 2729 int depth = 0; 2730 PrototypeObject pt = type._payload._object; 2731 while(pt !is null) { 2732 depth++; 2733 pt = pt.prototype; 2734 } 2735 2736 if(type._payload._object is arg._payload._object) 2737 thisScore += 2 + depth; 2738 else { 2739 if(arg._payload._object is null) 2740 return 0; // null sucks. 2741 2742 auto test = type._payload._object.prototype; 2743 // generic object compared against generic object matches 2744 if(test is null && type._payload._object.prototype is null) 2745 thisScore += 1; 2746 else { 2747 pt = arg._payload._object; 2748 while(pt !is null) { 2749 if(pt is test) { 2750 thisScore += 1 + depth; 2751 break; 2752 } 2753 pt = pt.prototype; 2754 } 2755 } 2756 } 2757 } else { 2758 thisScore += 3; // exact match without implicit conversion 2759 } 2760 } 2761 2762 return thisScore; 2763 } 2764 2765 /++ 2766 Does dynamic dispatch to overloads in a jsvar function set. 2767 2768 History: 2769 Added September 1, 2020. 2770 +/ 2771 class OverloadSet : PrototypeObject { 2772 this() { 2773 _properties["opCall"] = &opCall; 2774 _properties["apply"] = &apply; 2775 } 2776 2777 /// 2778 void addIndividualOverload(alias f)() { 2779 var func = &f; 2780 var[] argTypes; 2781 static if(is(typeof(f) Params == __parameters)) 2782 foreach(param; Params) 2783 argTypes ~= var(param.init); 2784 //import std.stdio; writeln("registered ", argTypes); 2785 overloads ~= Overload(argTypes, func); 2786 } 2787 2788 /// 2789 static if(__traits(compiles, mixin(q{ () { static foreach(i; [1,2]) {} } }) )) 2790 mixin(q{ 2791 void addOverloadsOf(alias what)() { 2792 foreach(alias f; __traits(getOverloads, __traits(parent, what), __traits(identifier, what))) 2793 addIndividualOverload!f; 2794 } 2795 }); 2796 2797 static struct Overload { 2798 // I don't even store the arity of a function object 2799 // so argTypes is the nest best thing. 2800 var[] argTypes; 2801 var func; 2802 } 2803 Overload[] overloads; 2804 2805 private bool exactMatch(var[] a, var[] b) { 2806 if(a.length != b.length) 2807 return false; 2808 foreach(i; 0 .. a.length) { 2809 if(a[i] !is b[i]) 2810 return false; 2811 } 2812 return true; 2813 } 2814 2815 void addOverload(Overload o) { 2816 foreach(ref ov; overloads) 2817 if(exactMatch(ov.argTypes, o.argTypes)) { 2818 ov.func = o.func; 2819 return; 2820 } 2821 overloads ~= o; 2822 } 2823 2824 /* 2825 I might have to add Object, Exception, and others to jsvar to represent types. 2826 maybe even int and such. 2827 2828 An object should probably have a secret property that gives its name... 2829 */ 2830 var apply(var this_, var[] arguments) { 2831 return opCall(arguments[0], arguments[1].get!(var[])); 2832 } 2833 2834 var opCall(var this_, var[] arguments) { 2835 // remember script.d supports default args too. 2836 int bestScore = int.min; 2837 Overload bestMatch; 2838 2839 if(overloads.length == 0) { 2840 return var(null); 2841 } 2842 2843 foreach(overload; overloads) { 2844 if(overload.argTypes.length == 0) { 2845 if(arguments.length == 0) { 2846 bestScore = 0; 2847 bestMatch = overload; 2848 break; 2849 } 2850 if(bestScore < 0) { 2851 bestScore = 0; 2852 bestMatch = overload; 2853 continue; 2854 } 2855 } 2856 2857 int thisScore = 0; 2858 foreach(idx, arg; arguments) { 2859 if(idx >= overload.argTypes.length) { 2860 thisScore = 0; 2861 break; 2862 } 2863 2864 // now if it is an object, if we can match, add score based on how derived the specified type is. 2865 // if it is the very same object, that's the best match possible. (prototype chain length + 1. and the one base point for matching at all.) 2866 // then if not, if the arg given can implicitly convert to the arg specified, give points based on how many prototypes the arg specified has. (plus one base point for matching at all) 2867 2868 // otherwise just give one point. 2869 2870 auto s = typeCompatibilityScore(arg, overload.argTypes[idx]); 2871 if(s == 0) { 2872 thisScore = 0; 2873 break; 2874 } 2875 thisScore += s; 2876 } 2877 2878 if(thisScore > 0 && thisScore > bestScore) { 2879 bestScore = thisScore; 2880 bestMatch = overload; 2881 } 2882 } 2883 2884 if(bestScore < 0) 2885 throw new Exception("no matching overload found " ~ to!string(arguments) ~ " " ~ to!string(overloads)); 2886 2887 2888 return bestMatch.func.apply(this_, arguments); 2889 } 2890 } 2891 2892 unittest { 2893 struct A { 2894 static: 2895 string foo(var arg) { return "generic"; } 2896 string foo(string s) { return "string"; } 2897 string foo(int i) { return "int"; } 2898 string foo(float i) { return "float"; } 2899 } 2900 2901 auto os = new OverloadSet(); 2902 os.addOverloadsOf!(A.foo); 2903 var g = var.emptyObject; 2904 g.foo = os; 2905 2906 //g.foo()(); 2907 assert(g.foo()("for me") == "string"); 2908 //g.foo()("for me", "lol"); 2909 assert(g.foo()(1) == "int"); 2910 assert(g.foo()(5.4) == "float"); 2911 assert(g.foo()(new Object) == "generic"); 2912 } 2913 2914 unittest { 2915 import std.typecons; 2916 2917 static struct A { 2918 int a; 2919 Nullable!int b; 2920 } 2921 2922 var a = A(3); 2923 assert(a.a == 3); 2924 assert(a.b == null); 2925 a = A(5, nullable(4)); 2926 assert(a.a == 5); 2927 assert(a.b == 4); 2928 } 2929 2930 bool appearsNumeric(string n) { 2931 if(n.length == 0) 2932 return false; 2933 foreach(c; n) { 2934 if(c < '0' || c > '9') 2935 return false; 2936 } 2937 return true; 2938 } 2939 2940 2941 /// Wraps a struct by reference. The pointer is stored - be sure the struct doesn't get freed or go out of scope! 2942 /// 2943 /// BTW: structs by value can be put in vars with var.opAssign and var.get. It will generate an object with the same fields. The difference is changes to the jsvar won't be reflected in the original struct and native methods won't work if you do it that way. 2944 WrappedNativeObject wrapNativeObject(Struct)(Struct* obj) if(is(Struct == struct)) { 2945 return null; // FIXME 2946 } 2947 2948 /+ 2949 _IDX_ 2950 2951 static_foreach(T.length, q{ 2952 mixin(q{ 2953 void 2954 } ~ __traits(identifier, T[_IDX_]) ~ q{ 2955 2956 } 2957 }); 2958 +/ 2959 2960 private 2961 string static_foreach(size_t length, int t_start_idx, int t_end_idx, string[] t...) pure { 2962 assert(__ctfe); 2963 int slen; 2964 int tlen; 2965 foreach(idx, i; t[0 .. t_start_idx]) 2966 slen += i.length; 2967 foreach(idx, i; t[t_start_idx .. $ + t_end_idx]) { 2968 if(idx) 2969 tlen += 5; 2970 tlen += i.length; 2971 } 2972 foreach(idx, i; t[$ + t_end_idx .. $]) 2973 slen += i.length; 2974 2975 char[] a = new char[](tlen * length + slen); 2976 2977 int loc; 2978 char[5] stringCounter; 2979 stringCounter[] = "00000"[]; 2980 2981 foreach(part; t[0 .. t_start_idx]) { 2982 a[loc .. loc + part.length] = part[]; 2983 loc += part.length; 2984 } 2985 2986 foreach(i; 0 .. length) { 2987 foreach(idx, part; t[t_start_idx .. $ + t_end_idx]) { 2988 if(idx) { 2989 a[loc .. loc + stringCounter.length] = stringCounter[]; 2990 loc += stringCounter.length; 2991 } 2992 a[loc .. loc + part.length] = part[]; 2993 loc += part.length; 2994 } 2995 2996 auto pos = stringCounter.length; 2997 while(pos) { 2998 pos--; 2999 if(stringCounter[pos] == '9') { 3000 stringCounter[pos] = '0'; 3001 } else { 3002 stringCounter[pos] ++; 3003 break; 3004 } 3005 } 3006 while(pos) 3007 stringCounter[--pos] = ' '; 3008 } 3009 3010 foreach(part; t[$ + t_end_idx .. $]) { 3011 a[loc .. loc + part.length] = part[]; 3012 loc += part.length; 3013 } 3014 3015 return a; 3016 } 3017 3018 private string deprecationCheck() { 3019 //https://issues.dlang.org/show_bug.cgi?id=22011 this hack sucks 3020 static if(__VERSION__ >= 2077) 3021 return "__traits(isDeprecated, __traits(getMember, obj, memberName == `this` ? `__ctor` : memberName))"; 3022 else 3023 return "false"; 3024 } 3025 3026 // LOL this can't work because function pointers drop the default :( 3027 private 3028 auto ParamDefault(alias T, size_t idx)() { 3029 static if(is(typeof(T) Params == __parameters)) { 3030 auto fn(Params[idx .. idx + 1] _args__) { // if i used plain `args` and one of the args in the list was also called `args`, this fails to compile and that error will be dropped by opDispatch. LOL. 3031 return _args__[0]; 3032 } 3033 static if(__traits(compiles, fn())) { 3034 return fn(); 3035 } else { 3036 return Params[idx].init; 3037 } 3038 } else static assert(0); 3039 }