1 Metamethods let values that carry a prototype (objects, arrays and resources) 2 customize how the ucode interpreter handles them. A metamethod is a regular 3 function stored under a reserved *dunder* name on a prototype (or directly on 4 an instance); the interpreter looks it up and invokes it automatically when a 5 certain operation is performed on the value. 6 7 With metamethods, a prototype can make its instances callable, synthesize 8 properties that are not stored anywhere, route property writes and deletions 9 to a backing store, or control how the value is rendered as a string. 10 11 ``` 12 let foo = proto({}, { 13 __call__(...args) { return "called with " + args; }, 14 __get__(key) { return key + " is virtual"; }, 15 __set__(key, val) { rawset(this, key, val); }, 16 __delete__(key) { return rawdelete(this, key); }, 17 __tostring__() { return "<my-obj>"; }, 18 }); 19 20 foo(1, 2); // "called with [ 1, 2 ]" 21 foo.missing; // "missing is virtual" 22 foo.other = 42; // routed to __set__ 23 delete foo.ghost; // not an own key, routed to __delete__ 24 print(foo); // <my-obj> 25 ``` 26 27 ucode currently supports five metamethods: 28 29 | Metamethod | Operation customized | 30 |----------------|--------------------------------------------| 31 | `__call__` | calling the value as a function | 32 | `__get__` | reading a property that is not found | 33 | `__set__` | writing a property that is not an own key | 34 | `__delete__` | deleting a property that is not an own key | 35 | `__tostring__` | rendering the value as a string | 36 37 Relational and arithmetic metamethods (`__lt__`, `__eq__`, `__add__`, ...) 38 are not part of the language. 39 40 ## Key Characteristics of Ucode Metamethods 41 42 ### Metamethods Are Strict Fallbacks 43 44 A property found by the normal lookup always wins over a metamethod. Each 45 metamethod is consulted only when the operation cannot be completed by the 46 normal mechanism: 47 48 | Operation | Normal mechanism | Metamethod consulted when | 49 |---------------------|---------------------------------------|--------------------------------------| 50 | `foo.bar` (read) | own key, then prototype chain | key not found anywhere in the chain | 51 | `foo.bar = x` (write) | direct store into `foo` | key is not already an own key of `foo` | 52 | `delete foo.bar` | direct delete from `foo` | key is not an own key of `foo` | 53 | `foo(...)` (call) | `foo` is a function | `foo` is not callable at all | 54 | string rendering | type-specific formatting | a `__tostring__` is present | 55 56 As a consequence, giving an object a `__get__` does not re-route reads of its 57 existing properties through script code; only genuinely missing keys reach the 58 metamethod. Real properties and metamethods can coexist on the same value, and 59 the real ones always win. In particular, reading `foo.__get__` as a plain 60 property returns the function itself, so metamethods are not hidden from 61 inspection. 62 63 Because `__set__` only fires for keys the instance does not own, reassigning 64 an existing key is a fast direct store; the metamethod is bypassed. This 65 asymmetry is intentional and keeps property writes on plain objects free of 66 any overhead. 67 68 ### Resolution Walks the Full Prototype Chain 69 70 The metamethod itself is found by walking the value's entire prototype chain, 71 from the direct prototype to the most distant one. The first hit wins. 72 73 ``` 74 let grand = { __get__(key) { return "grand:" + key; } }; 75 let parent = proto({}, grand); 76 let child = proto({}, parent); 77 78 child.x; // "grand:x" 79 ``` 80 81 Metamethods are looked up on the prototype chain only; the value's own keys 82 never count. An own property with a dunder name is plain data, not a 83 metamethod: 84 85 ``` 86 let o = proto({}, { __get__(key) { return "parent"; } }); 87 88 o.__get__ = function(key) { return "own"; }; 89 90 o.x; // "parent" - the own property does not shadow 91 o.__get__; // the function itself, readable like any property 92 ``` 93 94 This keeps dunder names inert unless a prototype explicitly defines them: 95 ordinary property writes (mixin loops, data copied from external sources, a 96 function stored under a dunder name) cannot accidentally change how a value 97 behaves. To customize a single instance, give it its own prototype: 98 `o = proto(o, { __get__(key) { ... } })`. 99 100 ### Invocation Convention 101 102 All metamethods are invoked as method calls on the instance: `this` is the 103 instance (not the prototype), and the operation-specific arguments follow. 104 105 | Metamethod | Invocation | Expected return value | 106 |----------------|------------------------|---------------------------------------------| 107 | `__call__` | `m.__call__(...args)` | the result of the call | 108 | `__get__` | `m.__get__(key)` | the property value, or `null` for "still missing" | 109 | `__set__` | `m.__set__(key, value)`| ignored; the assignment yields `value` | 110 | `__delete__` | `m.__delete__(key)` | truthy = deleted, falsy = not present | 111 | `__tostring__` | `m.__tostring__()` | a string (a non-string result renders as an empty string) | 112 113 The *key* argument of `__get__`, `__set__` and `__delete__` is the original 114 key value the script used: a string for `foo.bar`, a number for `foo[0]`. 115 Metamethods are looked up by exact key, without any coercion. 116 117 A metamethod is only honored when it is an actual function (closure or 118 native function). The chain walk stops at the first prototype holding the 119 dunder name, so a non-callable value stored there ends the search: the 120 operation falls back to the default behaviour and prototypes further up the 121 chain are not consulted. A slot holding `null` is treated as absent; the 122 search continues past it: 123 124 ``` 125 let grand = { __get__(key) { return "grand:" + key; } }; 126 let parent = proto({ __get__: "not callable" }, grand); 127 let child = proto({}, parent); 128 129 child.x; // null - the non-callable slot ends the search, 130 // the grandparent's __get__ is not consulted 131 ``` 132 133 ### The Assignment Expression Always Yields the Assigned Value 134 135 An assignment in ucode evaluates to the value being stored (`a.x = 5` yields 136 `5`). When a `__set__` dispatch handles `foo.bar = x`, the interpreter pushes 137 `x` as the expression's value and discards whatever `__set__` returned. A 138 `__set__` that forgets to return, returns `null`, or returns a coerced value 139 still yields `x` from the assignment expression: 140 141 ``` 142 let o = proto({}, { 143 __set__(key, val) { 144 rawset(this, key, val * 2); 145 return "ignored"; 146 } 147 }); 148 149 o.x = 21; // the expression yields 21 150 o.x; // 42 151 ``` 152 153 A `__set__` that does not want to store a value must raise an exception; the 154 assignment then fails with that exception. 155 156 ## The `__call__` Metamethod 157 158 `__call__` makes a value invocable. It is invoked as a method on the instance 159 and receives the call arguments: 160 161 ``` 162 let c = proto({ base: 10 }, { 163 __call__(a, b) { 164 return this.base + a + b; 165 } 166 }); 167 168 c(1, 2); // 13 169 ``` 170 171 `this` inside `__call__` is always the value being called: 172 173 ``` 174 let inner = proto({ tag: "inner" }, { 175 __call__(n) { return this.tag + ":" + n; } 176 }); 177 178 let outer = { m: inner }; 179 180 outer.m(7); // "inner:7", `this` is the `inner` value 181 inner("x"); // "inner:x" 182 ``` 183 184 A value which holds itself as its own `__call__` does not become callable, 185 since only actual functions count as metamethods: 186 187 ``` 188 let c = {}; 189 c.__call__ = c; 190 191 c(); // error: left-hand side is not a function 192 ``` 193 194 Because the interpreter's call path itself dispatches `__call__`, callable 195 objects work everywhere a function is accepted, including as callbacks for 196 builtins: 197 198 ``` 199 let add1 = proto({}, { __call__(x) { return x + 1; } }); 200 201 map([1, 2, 3], add1); // [ 2, 3, 4 ] 202 ``` 203 204 One caveat: a `__call__` that tail-calls the same instance 205 206 ``` 207 let foo = proto({}, { __call__() { return foo(); } }); 208 foo(); 209 ``` 210 211 loops forever without tripping the interpreter's recursion limit, because the 212 tail call reuses the current frame. This is the same class of behavior as a 213 plain tail-recursive function, but the re-dispatch is implicit, so it is easy 214 to write by accident. 215 216 ## The `__get__` Metamethod 217 218 `__get__` is consulted when a property lookup finds nothing in the instance 219 itself nor in its prototype chain. It is invoked as a method on the instance 220 with the key as its single argument, and its return value becomes the result 221 of the read; returning `null` means "still missing": 222 223 ``` 224 let o = proto({}, { 225 __get__(key) { 226 return key == "bar" ? "virtual:bar" : null; 227 } 228 }); 229 230 o.bar; // "virtual:bar" 231 o.foo; // null 232 ``` 233 234 Existing properties shadow the metamethod, both own ones and ones inherited 235 from the prototype chain: 236 237 ``` 238 let hits = []; 239 240 let o = proto({ own: 1 }, { 241 inherited: 2, 242 __get__(key) { 243 push(hits, key); 244 return "virtual"; 245 } 246 }); 247 248 o.own; // 1 249 o.inherited; // 2 250 o.missing; // "virtual" 251 hits; // [ "missing" ] 252 ``` 253 254 ### Delegating to an Object 255 256 A metamethod slot may hold an object or an array instead of a function. When 257 the `__get__` slot holds an object, a lookup that reaches it re-dispatches on 258 that object with the same key - exactly like Lua's `__index`. This is a 259 convenient way to share a common set of default properties without copying 260 them: 261 262 ``` 263 let defaults = { host: "0.0.0.0", port: 80 }; 264 let o = proto({}, { __get__: defaults }); 265 266 o.host; // "0.0.0.0", delegated to the defaults object 267 o.port; // 80 268 o.nope; // null, not found there either 269 ``` 270 271 The delegated lookup is a fresh read, so the object may hold its own keys and 272 even its own metamethods. A `__get__` *function*, on the other hand, simply 273 hands its return value over as the result of the read; returning an object 274 does not look into it: 275 276 ``` 277 let base = { greeting: "hi" }; 278 let o = proto({}, { __get__(key) { return base; } }); 279 280 o.greeting; // the base object itself, not "hi" 281 ``` 282 283 The same slot-holds-an-object form works for the other key metamethods: an 284 object-valued `__set__` slot redirects stores into that object, and an 285 object-valued `__delete__` slot re-dispatches deletes on it. 286 287 ### Optional Chaining 288 289 Optional reads go through the same lookup, so `?.` yields virtual properties 290 too: 291 292 ``` 293 let o = proto({}, { __get__(key) { return "virtual:" + key; } }); 294 295 o?.y; // "virtual:y" 296 ``` 297 298 ### Compound Assignment 299 300 A compound assignment such as `foo.bar += 1` dispatches one read and one 301 write of the same key, so both `__get__` and `__set__` are consulted: 302 303 ``` 304 let log = []; 305 306 let o = proto({}, { 307 __get__(key) { 308 push(log, "get:" + key); 309 return rawget(this, "_" + key); 310 }, 311 __set__(key, val) { 312 push(log, "set:" + key + "=" + val); 313 rawset(this, "_" + key, val); 314 } 315 }); 316 317 o.n += 5; 318 o.n *= 2; 319 320 print(log); // [ "get:n", "set:n=5", "get:n", "set:n=10" ] 321 o.n; // 10 322 ``` 323 324 If the read half raises, the write half is skipped and the exception 325 propagates instead of storing a value derived from a read that never 326 happened. 327 328 ## The `__set__` Metamethod 329 330 `__set__` is consulted when a property is assigned which is not already an 331 own property of the object. It is invoked as a method on the instance with 332 the key and the value as arguments: 333 334 ``` 335 let log = []; 336 337 let o = proto({ own: 1 }, { 338 __set__(key, val) { 339 push(log, key + "=" + val); 340 rawset(this, "backing:" + key, val); 341 } 342 }); 343 344 o.nw = 2; // routed to __set__ 345 o.own = 3; // own key, direct store, no dispatch 346 347 rawget(o, "backing:nw"); // 2 348 o.own; // 3 349 print(log); // [ "nw=2" ] 350 ``` 351 352 Two details are worth keeping in mind: 353 354 * Writing a key that exists on a *prototype* but not on the instance does 355 dispatch `__set__`. If the metamethod wants the usual shadowing behavior 356 (create an own key), it stores it with `rawset(this, key, val)`; a plain 357 `this[key] = val` would re-dispatch `__set__` and recurse (see 358 [Raw Accessors](#raw-accessors)). 359 * Object literals store their members without dispatching `__set__`, so a 360 literal may define the metamethod it is protected by: 361 362 ``` 363 let o = { 364 __set__(key, val) { 365 die("intercepted " + key); 366 }, 367 a: 1, 368 b: null 369 }; 370 371 o.a; // 1, the literal construction did not dispatch 372 ``` 373 374 A `null` value is stored and read back like any other value. 375 376 ## The `__delete__` Metamethod 377 378 `__delete__` is consulted when `delete` is applied to a key the instance does 379 not own. It is invoked as a method on the instance with the key as its single 380 argument; a truthy return value means "deleted", a falsy one means "not 381 present": 382 383 ``` 384 let log = []; 385 386 let o = proto({ stored: 1 }, { 387 __delete__(key) { 388 push(log, key); 389 return rawdelete(this, key); 390 } 391 }); 392 393 delete o.stored; // own key, direct delete, no dispatch 394 delete o.ghost; // not an own key, routed to __delete__ 395 396 print(log); // [ "ghost" ] 397 ``` 398 399 Deleting an own key never dispatches; it is a direct delete. `delete` on 400 arrays is not supported (it raises "left-hand side expression is not an 401 array or object"), so `__delete__` never fires for array elements. 402 403 ## The `__tostring__` Metamethod 404 405 `__tostring__` controls how the value is rendered by `print()`, string 406 concatenation and the `%s`/`%J` format specifiers. It is invoked as a method 407 on the value with no arguments and must return a string: 408 409 ``` 410 let o = proto({}, { 411 __tostring__() { return "<my-obj>"; } 412 }); 413 414 print(o); // <my-obj> 415 "[" + o + "]"; // [<my-obj>] 416 printf("%s\n", o); // <my-obj> 417 printf("%.J\n", o); // "<my-obj>" 418 ``` 419 420 The legacy `tostring` name is still honored as an alias; `__tostring__` wins 421 when both are present. Both are now resolved by walking the full prototype 422 chain, so a `tostring` on a grandparent prototype works as well. 423 424 A broken `__tostring__` never produces a broken rendering: a non-callable 425 method falls back to the default rendering, and a call that raises falls back 426 to the default rendering as well - but the exception itself still propagates 427 to the caller, so `print()` aborts unless the caller catches it. A non-string 428 result renders as an empty string. 429 430 ## Metamethods on Arrays 431 432 Arrays keep their dense storage: a key which is a valid numeric index never 433 dispatches a metamethod at all, whether the element exists or not. Only keys 434 which are no valid index (such as `1.5`, `"abc"` or any other non-numeric 435 name) are passed to `__get__` and `__set__`: 436 437 ``` 438 let log = []; 439 let store = {}; 440 441 let a = proto([1, 2], { 442 __get__(key) { 443 push(log, "get:" + key); 444 return rawget(store, key); 445 }, 446 __set__(key, val) { 447 push(log, "set:" + key); 448 rawset(store, key, val); 449 } 450 }); 451 452 a[2] = 3; // raw index store, no __set__ 453 a[-1] = 4; // raw index store, no __set__ 454 a[9]; // null, raw index read, no __get__ 455 a.name = "stored"; // __set__("name", "stored") 456 a.name; // "stored", via __get__ from the backing store 457 458 print(log); // [ "set:name", "get:name" ] 459 ``` 460 461 Note that the named fields cannot live on the array itself: `rawset()` on an 462 array only accepts index keys, so a `__set__` on an array must route its 463 values to some other storage, such as a plain object kept as a backing store 464 (closures capture it, as above). 465 466 A non-index property set on an array without a `__set__` metamethod is 467 guaranteed to fail (arrays have no own-key storage to fall back to). In 468 non-strict mode the write is silently dropped; in strict mode it raises a 469 Type error, consistent with setting a property on any other non-object type: 470 471 ``` 472 "use strict"; 473 let a = [1, 2]; 474 a.foo = 3; // Type error: attempt to set property on array value 475 a[2] = 3; // fine: valid index 476 a[-5] = 4; // fine: out-of-range negative index, reads back as null 477 ``` 478 479 Adding a `__set__` metamethod to the array's prototype silences the error 480 and gives the script a chance to route the value to its own storage. 481 482 This keeps integer-indexed access a fast O(1) slot read and reserves 483 `__get__`/`__set__` for the "array as a named-field container" case. Sparse 484 or numeric virtual properties are not supported on arrays; use an object for 485 that. 486 487 The array builtins `push()`, `unshift()`, `pop()`, `shift()` and `splice()` 488 are bulk structural operations and do not dispatch metamethods either. 489 490 ## The `in` Operator 491 492 The `in` operator tests keys and never dispatches metamethods. For objects it 493 walks the full prototype chain: 494 495 ``` 496 let o = proto({ own: 1 }, { 497 inherited: 2, 498 __get__(key) { return "virtual"; } 499 }); 500 501 "own" in o; // true 502 "inherited" in o; // true 503 "virtual" in o; // false, purely-virtual properties are invisible to `in` 504 "__get__" in o; // true, metamethods are stored properties 505 ``` 506 507 For arrays, `in` tests element values first and keys of the prototype chain 508 second: 509 510 ``` 511 let a = proto([1, 2], { named: 3 }); 512 513 1 in a; // true, element 514 "named" in a; // true, prototype key 515 3 in a; // false 516 ``` 517 518 A property that exists only as a virtual one (synthesized by `__get__` from 519 nothing) is not visible to `in`. 520 521 ## Raw Accessors 522 523 A metamethod that re-enters the very operation it customizes re-dispatches 524 itself. There is no re-entrancy guard: such recursion continues until the 525 interpreter's call depth limit is exceeded, which raises "Too much recursion" 526 instead of hanging: 527 528 ``` 529 let o = proto({}, { __get__(key) { return this[key]; } }); 530 531 o.x; // error: Too much recursion 532 ``` 533 534 The escape hatch is the pair of *raw accessors*, which perform the underlying 535 read, write or delete without dispatching any metamethod. They are exposed to 536 scripts as the builtins `rawget()`, `rawset()` and `rawdelete()`: 537 538 #### {@link module:core#rawget|rawget(obj, key)} → {*} 539 540 Reads the property the way the interpreter would if `obj` had no `__get__` in 541 its prototype chain (array index, resource type-proto, or object own key plus 542 prototype chain), without dispatching `__get__`. Returns `null` if the 543 property is not present. 544 545 #### {@link module:core#rawset|rawset(obj, key, val)} → {*} 546 547 Stores the property the way the interpreter would if `obj` had no `__set__` 548 in its prototype chain (own-key store for objects, index store for arrays), 549 without dispatching `__set__`. Returns the stored value, or `null` on failure 550 (such as a non-container `obj` or a non-index key on an array). 551 552 #### {@link module:core#rawdelete|rawdelete(obj, key)} → {boolean} 553 554 Deletes the own property the way the interpreter would if `obj` had no 555 `__delete__` in its prototype chain, without dispatching `__delete__`. 556 Returns `true` if a property was deleted, `false` otherwise. 557 558 With the raw accessors, a metamethod can keep its backing store in the 559 instance itself, which is the idiomatic pattern for all three: 560 561 ``` 562 let o = proto({}, { 563 __get__(key) { 564 let v = rawget(this, "_" + key); 565 return v === null ? "computed:" + key : v; 566 }, 567 __set__(key, val) { 568 rawset(this, "_" + key, val * 10); 569 }, 570 __delete__(key) { 571 return rawdelete(this, "_" + key); 572 } 573 }); 574 575 o.n = 4; 576 o.n; // 40 577 o.other; // "computed:other" 578 delete o.n; // true 579 ``` 580 581 Nested dispatch on *different* keys is unrestricted and a normal, useful 582 pattern: a `__get__("a")` that reads `this.b` and triggers `__get__("b")` 583 simply recurses one frame deeper and returns. 584 585 ## Exceptions and Recursion 586 587 Exceptions raised by a metamethod fail the operation and propagate to its 588 caller, for reads, writes and deletes alike, leaving the interpreter fully 589 usable afterwards: 590 591 ``` 592 let o = proto({}, { 593 __get__(key) { die("get:" + key); }, 594 __set__(key, val) { die("set:" + key); }, 595 __delete__(key) { die("del:" + key); } 596 }); 597 598 try { o.a; } catch (e) { print(e); } // get:a 599 try { o.a = 1; } catch (e) { print(e); } // set:a 600 try { delete o.a; } catch (e) { print(e); } // del:a 601 try { o.a += 1; } catch (e) { print(e); } // get:a 602 ``` 603 604 A metamethod that recurses too deeply fails with "Too much recursion", which 605 unwinds the dispatch and surfaces as the operation's error rather than 606 looping forever. Rendering a value while its own `__tostring__` is running 607 re-dispatches as well: a `__tostring__` that renders its own value - for 608 example through concatenation - recurses until the interpreter raises "Too 609 much recursion". 610 611 ## Summary 612 613 | Case | Outcome | 614 |-------------------------------------------------|----------------------------------------------| 615 | `foo.bar` where `bar` is own or inherited | normal value, `__get__` not consulted | 616 | `foo.bar` missing, `__get__` set | `__get__("bar")` result, `this` = `foo` | 617 | `__get__` returns `null` | `null`, indistinguishable from not found | 618 | `__get__` slot holds an object or array | lookup re-dispatches on that object | 619 | `__get__` returns an object or array | handed over as-is, not looked into | 620 | `foo.bar = x`, `bar` an own key | direct store, `__set__` not consulted | 621 | `foo.bar = x`, `bar` inherited or missing | `__set__("bar", x)`, `this` = `foo` | 622 | `foo.bar = x` again after the first write | direct store, `__set__` bypassed | 623 | `__set__` return value | ignored, the expression yields `x` | 624 | `delete foo.bar`, `bar` an own key | direct delete | 625 | `delete foo.bar`, `bar` not own | `__delete__("bar")`, truthy ⇒ `true` | 626 | `foo(...)` where `foo` is a function | normal call, `__call__` not consulted | 627 | `foo(...)` where `foo` has `__call__` | `__call__(...)`, `this` = `foo` | 628 | `print(foo)` with `__tostring__` | `__tostring__()` result | 629 | `print(foo)` with legacy `tostring` | still works (alias) | 630 | `arr[i] = x`, `arr[i]`, `delete arr[i]` | raw index store/read; delete is not supported | 631 | `arr["name"] = x`, `arr["name"]` | `__set__("name", x)` / `__get__("name")` | 632 | `x in foo` | key/element test, never dispatches | 633 | `rawget`/`rawset`/`rawdelete` | storage access without dispatch |
This page was automatically generated by LXR 0.3.1. • OpenWrt