diff --git a/design/mvp/Async.md b/design/mvp/Async.md index e948b741..1ce45b16 100644 --- a/design/mvp/Async.md +++ b/design/mvp/Async.md @@ -620,58 +620,68 @@ which to use by default. ### Async Import ABI -Given an imported WIT function: +Given these imported WIT functions (using the fixed-length-list feature 🔧): ```wit world w { - import foo: func(s: string) -> string; + import foo: func(s: string) -> u32; + import bar: func(s: string) -> string; + import baz: func(t: list) -> string; + import quux: func(t: list) -> string; } ``` -the default sync import function signature is: +the default/synchronous lowered import function signatures are: ```wat ;; sync -(func (param $s-ptr i32) (param $s-len i32) (param $out i32)) +(func $foo (param $s-ptr i32) (param $s-len i32) (result i32)) +(func $bar (param $s-ptr i32) (param $s-len i32) (param $out-ptr i32)) +(func $baz (param i64 i64 i64 i64 i64) (param $out-ptr i32)) +(func $quux (param $in-ptr i32) (param $out-ptr i32)) ``` -where `$out` must be a 4-byte-aligned pointer into linear memory into which the -8-byte (pointer, length) of the returned string will be stored. - -The new async import function signature is: +Here: `foo`, `bar` and `baz` pass their parameters as "flattened" core value +types while `quux` passes its parameters via the `$in-ptr` linear memory +pointer (due to the Canonical ABI limitation of 16 maximum flattened +parameters). Similarly, `foo` returns its result as a single core value while +`bar`, `baz` and `quux` return their results via the `$out-ptr` linear memory +pointer (due to the current Canonical ABI limitation of 1 maximum flattened +result). + +The corresponding asynchronous lowered import function signatures are: ```wat ;; async -(func (param $in i32) (param $out i32) (result i32)) +(func $foo (param $s-ptr i32) (param $s-len i32) (param $out-ptr i32) (result i32)) +(func $bar (param $s-ptr i32) (param $s-len i32) (param $out-ptr i32) (result i32)) +(func $baz (param $in-ptr i32) (param $out-ptr i32) (result i32)) +(func $quux (param $in-ptr i32) (param $out-ptr i32) (result i32)) ``` -where `$in` must be a 4-byte-aligned pointer into linear memory from which the -8-byte (pointer, length) of the string argument will be loaded and `$out` works -the same as in the synchronous case. What's different, however, is *when* `$in` -and `$out` are read or written. In a synchronous call, they are always read or -written before the call returns. In an asynchronous call, there is a set of -possibilities indicated by the `(result i32)` value: -* If the returned `i32` is `2`, then the call returned eagerly without - blocking and so `$in` has been read and `$out` has been written. -* Otherwise, the high 28 bits of the `i32` are the index of a new `Subtask` - in the current component instance's table. The low 4 bits indicate how far - the callee made it before blocking: - * If `1`, the callee didn't even start (due to backpressure), and thus - neither `$in` nor `$out` have been accessed yet. - * If `2`, the callee started by reading `$in`, but blocked before writing - `$out`. - -The async signature `(func (param i32 i32) (result i32))` is the same for -almost all WIT function types since the ABI stores everything in linear memory. -However, there are three special cases: -* If the WIT parameter list is empty, `$in` is removed. -* If the WIT parameter list flattens to exactly 1 core value type (`i32` or - otherwise), `$in` uses that core value type and the argument is passed - by value. -* If the WIT result is empty, `$out` is removed. - -For example: +Comparing signatures, the differences are: +* Async-lowered functions have a maximum of 4 flat parameters (not 16). +* Async-lowered functions always return their value via linear memory pointer. +* Async-lowered functions always have a single `i32` "status" code. + +Additionally, *when* the parameter and result pointers are read/written depends +on the status code: +* If the low 4 bits of the status are `0`, the call didn't even start and so + `$in-ptr` hasn't been read and `$out-ptr` hasn't been written and the high + 28 bits are the index of a new async subtask to wait on. +* If the low 4 bits of the status are `1`, the call started, `$in-ptr` was + read, but `$out-ptr` hasn't been written and the high 28 bits are the index + of a new async subtask to wait on. +* If the low 4 bits of the status are `2`, the call returned and so `$in-ptr` + and `$out-ptr` have been read/written and the high 28 bits are `0` because + there is no async subtask to wait on. + +When a parameter/result pointer hasn't yet been read/written, the async caller +must take care to keep the region of memory allocated to the call until +receiving an event indicating that the async subtask has started/returned. + +Other example asynchronous lowered signatures: + | WIT function type | Async ABI | | ----------------------------------------- | --------------------- | | `func()` | `(func (result i32))` | -| `func() -> string` | `(func (param $out i32) (result i32))` | -| `func(s: string)` | `(func (param $in i32) (result i32))` | -| `func(x: f32) -> f32` | `(func (param $in f32) (param $out i32) (result i32))` | -| `func(x: list>) -> list` | `(func (param $in i32) (param $out i32) (result i32))` | +| `func() -> string` | `(func (param $out-ptr i32) (result i32))` | +| `func(x: f32) -> f32` | `(func (param $x f32) (param $out-ptr i32) (result i32))` | +| `func(s: string, t: string)` | `(func (param $s-ptr i32) (param $s-len i32) (result $t-ptr i32) (param $t-len i32) (result i32))` | `future` and `stream` can appear anywhere in the parameter or result types. For example: ```wit @@ -689,11 +699,11 @@ the synchronous ABI has signature: ``` and the asynchronous ABI has the signature: ```wat -(func (param $in i32) (param $out i32) (result i32)) +(func (param $f i32) (param $out-ptr i32) (result i32)) ``` -where, according to the above rules, `$in` is the index of a future in the -current component instance's table (not a pointer to one) while `$out` is a -pointer to a linear memory location that will receive an `i32` index. +where `$f` is the index of a future (not a pointer to one) while while +`$out-ptr` is a pointer to a linear memory location that will receive an `i32` +index. For the runtime semantics of this `i32` index, see `lift_stream`, `lift_future`, `lower_stream` and `lower_future` in the [Canonical ABI @@ -792,7 +802,7 @@ with `...` to focus on the overall flow of function calls: (core module $Main (import "libc" "mem" (memory 1)) (import "libc" "realloc" (func (param i32 i32 i32 i32) (result i32))) - (import "" "fetch" (func $fetch (param i32 i32) (result i32))) + (import "" "fetch" (func $fetch (param i32 i32 i32) (result i32))) (import "" "waitable-set.new" (func $new_waitable_set (result i32))) (import "" "waitable-set.wait" (func $wait (param i32 i32) (result i32))) (import "" "waitable.join" (func $join (param i32 i32))) @@ -806,7 +816,7 @@ with `...` to focus on the overall flow of function calls: ... loop ... - call $fetch ;; pass a pointer-to-string and pointer-to-list-of-bytes outparam + call $fetch ;; pass a string pointer, string length and pointer-to-list-of-bytes outparam ... ;; ... and receive the index of a new async subtask global.get $wsi call $join ;; ... and add it to the waitable set @@ -884,7 +894,7 @@ not externally-visible behavior. (core module $Main (import "libc" "mem" (memory 1)) (import "libc" "realloc" (func (param i32 i32 i32 i32) (result i32))) - (import "" "fetch" (func $fetch (param i32 i32) (result i32))) + (import "" "fetch" (func $fetch (param i32 i32 i32) (result i32))) (import "" "waitable-set.new" (func $new_waitable_set (result i32))) (import "" "waitable.join" (func $join (param i32 i32))) (import "" "task.return" (func $task_return (param i32 i32))) @@ -897,7 +907,7 @@ not externally-visible behavior. ... loop ... - call $fetch ;; pass a pointer-to-string and pointer-to-list-of-bytes outparam + call $fetch ;; pass a string pointer, string length and pointer-to-list-of-bytes outparam ... ;; ... and receive the index of a new async subtask global.get $wsi call $join ;; ... and add it to the waitable set diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index a96aa7f6..50b3717b 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -2446,6 +2446,7 @@ stack), passing in an `i32` pointer as an parameter instead of returning an Given all this, the top-level definition of `flatten_functype` is: ```python MAX_FLAT_PARAMS = 16 +MAX_FLAT_ASYNC_PARAMS = 4 MAX_FLAT_RESULTS = 1 def flatten_functype(opts, ft, context): @@ -2465,12 +2466,14 @@ def flatten_functype(opts, ft, context): else: match context: case 'lift': + if len(flat_params) > MAX_FLAT_PARAMS: + flat_params = ['i32'] if opts.callback: flat_results = ['i32'] else: flat_results = [] case 'lower': - if len(flat_params) > 1: + if len(flat_params) > MAX_FLAT_ASYNC_PARAMS: flat_params = ['i32'] if len(flat_results) > 0: flat_params += ['i32'] @@ -3124,7 +3127,7 @@ always returns control flow back to the caller without blocking: ```python def on_start(): on_progress() - return lift_flat_values(cx, 1, flat_args, ft.param_types()) + return lift_flat_values(cx, MAX_FLAT_ASYNC_PARAMS, flat_args, ft.param_types()) def on_resolve(results): on_progress() diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index f6b57a43..79fea4d7 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -1521,6 +1521,7 @@ def lower_async_value(ReadableEndT, cx, v, t): ### Flattening MAX_FLAT_PARAMS = 16 +MAX_FLAT_ASYNC_PARAMS = 4 MAX_FLAT_RESULTS = 1 def flatten_functype(opts, ft, context): @@ -1540,12 +1541,14 @@ def flatten_functype(opts, ft, context): else: match context: case 'lift': + if len(flat_params) > MAX_FLAT_PARAMS: + flat_params = ['i32'] if opts.callback: flat_results = ['i32'] else: flat_results = [] case 'lower': - if len(flat_params) > 1: + if len(flat_params) > MAX_FLAT_ASYNC_PARAMS: flat_params = ['i32'] if len(flat_results) > 0: flat_params += ['i32'] @@ -1932,7 +1935,7 @@ def on_resolve(results): def on_start(): on_progress() - return lift_flat_values(cx, 1, flat_args, ft.param_types()) + return lift_flat_values(cx, MAX_FLAT_ASYNC_PARAMS, flat_args, ft.param_types()) def on_resolve(results): on_progress() diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index 8f194cf8..eb27c60c 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -2231,6 +2231,44 @@ async def core_func(task, args): await canon_lift(sync_opts, inst, ft, core_func, None, lambda:[], lambda _:(), host_on_block) +async def test_async_flat_params(): + heap = Heap(1000) + opts = mk_opts(heap.memory, 'utf8', heap.realloc, sync = False) + inst = ComponentInstance() + caller = Task(opts, inst, FuncType([],[]), None, None, None) + + ft1 = FuncType([F32Type(), F64Type(), U32Type(), S64Type()],[]) + async def f1(task, on_start, on_resolve, on_block): + args = on_start() + assert(len(args) == 4) + assert(args[0] == 1.1) + assert(args[1] == 2.2) + assert(args[2] == 3) + assert(args[3] == 4) + on_resolve([]) + [ret] = await canon_lower(opts, ft1, f1, caller, [1.1, 2.2, 3, 4]) + assert(ret == Subtask.State.RETURNED) + + ft2 = FuncType([U32Type(),U8Type(),U8Type(),U8Type()],[]) + async def f2(task, on_start, on_resolve, on_block): + args = on_start() + assert(len(args) == 4) + assert(args == [1,2,3,4]) + on_resolve([]) + [ret] = await canon_lower(opts, ft2, f2, caller, [1,2,3,4]) + assert(ret == Subtask.State.RETURNED) + + ft3 = FuncType([U32Type(),U8Type(),U8Type(),U8Type(),U8Type()],[]) + async def f3(task, on_start, on_resolve, on_block): + args = on_start() + assert(len(args) == 5) + assert(args == [1,2,3,4,5]) + on_resolve([]) + heap.memory[12:20] = b'\x01\x00\x00\x00\x02\x03\x04\x05' + [ret] = await canon_lower(opts, ft3, f3, caller, [12]) + assert(ret == Subtask.State.RETURNED) + + async def run_async_tests(): await test_roundtrips() await test_handles() @@ -2250,6 +2288,7 @@ async def run_async_tests(): await test_futures() await test_cancel_subtask() await test_self_empty() + await test_async_flat_params() asyncio.run(run_async_tests()) diff --git a/test/README.md b/test/README.md index a14bee8d..b454b825 100644 --- a/test/README.md +++ b/test/README.md @@ -4,7 +4,14 @@ This directory contains Component Model reference tests, grouped by functionalit ## Running in Wasmtime +All tests currently run and (except temporarily while spec changes are rolling out) +pass using the [`dev` release of `wasip3-prototyping`](https://github.com/bytecodealliance/wasip3-prototyping/releases/tag/dev). + A single `.wast` test can be run via: ``` wasmtime wast -W component-model-async=y the-test.wast ``` +All the tests can be run from this directory via: +``` +find . -name "*.wast" | xargs wasmtime-p3 wast -W component-model-async=y +``` diff --git a/test/async/async-calls-sync.wast b/test/async/async-calls-sync.wast index 2f4da2e4..14e7addb 100644 --- a/test/async/async-calls-sync.wast +++ b/test/async/async-calls-sync.wast @@ -119,8 +119,8 @@ (import "" "waitable.join" (func $waitable.join (param i32 i32))) (import "" "waitable-set.new" (func $waitable-set.new (result i32))) (import "" "unblock" (func $unblock)) - (import "" "sync-func1" (func $sync-func1 (param i32 i32) (result i32))) - (import "" "sync-func2" (func $sync-func2 (param i32 i32) (result i32))) + (import "" "sync-func1" (func $sync-func1 (param i32) (result i32))) + (import "" "sync-func2" (func $sync-func2 (param i32) (result i32))) (global $ws (mut i32) (i32.const 0)) (func $start (global.set $ws (call $waitable-set.new))) @@ -134,22 +134,22 @@ ;; call 'sync-func1' and 'sync-func2' asynchronously, both of which will block ;; (on $AsyncInner.blocking-call). because 'sync-func1/2' are in different instances, ;; both calls will reach the STARTED state. - (local.set $ret (call $sync-func1 (i32.const 0xdeadbeef) (i32.const 8))) + (local.set $ret (call $sync-func1 (i32.const 8))) (if (i32.ne (i32.const 0x21 (; STARTED=1 | (subtask=2 << 4) ;)) (local.get $ret)) (then unreachable)) (call $waitable.join (i32.const 2) (global.get $ws)) - (local.set $ret (call $sync-func2 (i32.const 0xdeadbeef) (i32.const 12))) + (local.set $ret (call $sync-func2 (i32.const 12))) (if (i32.ne (i32.const 0x31 (; STARTED=1 | (subtask=3 << 4) ;)) (local.get $ret)) (then unreachable)) (call $waitable.join (i32.const 3) (global.get $ws)) ;; now start another pair of 'sync-func1/2' calls, both of which should see auto ;; backpressure and get stuck in the STARTING state. - (local.set $ret (call $sync-func1 (i32.const 0xdeadbeef) (i32.const 16))) + (local.set $ret (call $sync-func1 (i32.const 16))) (if (i32.ne (i32.const 0x40 (; STARTING=0 | (subtask=4 << 4) ;)) (local.get $ret)) (then unreachable)) (call $waitable.join (i32.const 4) (global.get $ws)) - (local.set $ret (call $sync-func2 (i32.const 0xdeadbeef) (i32.const 20))) + (local.set $ret (call $sync-func2 (i32.const 20))) (if (i32.ne (i32.const 0x50 (; STARTING=0 | (subtask=5 << 4) ;)) (local.get $ret)) (then unreachable)) (call $waitable.join (i32.const 5) (global.get $ws)) diff --git a/test/async/cancel-subtask.wast b/test/async/cancel-subtask.wast index 3d494ee4..54ad413d 100644 --- a/test/async/cancel-subtask.wast +++ b/test/async/cancel-subtask.wast @@ -59,7 +59,7 @@ (import "" "mem" (memory 1)) (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) (import "" "subtask.drop" (func $subtask.drop (param i32))) - (import "" "f" (func $f (param i32 i32) (result i32))) + (import "" "f" (func $f (param i32) (result i32))) (func $run (export "run") (result i32) (local $ret i32) (local $retp i32) @@ -69,7 +69,7 @@ ;; call 'f'; it should block (local.set $retp (i32.const 4)) (i32.store (local.get $retp) (i32.const 0xbad0bad0)) - (local.set $ret (call $f (i32.const 0xdeadbeef) (local.get $retp))) + (local.set $ret (call $f (local.get $retp))) (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) (then unreachable)) (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) diff --git a/test/async/deadlock.wast b/test/async/deadlock.wast index 0c9861b8..e4697352 100644 --- a/test/async/deadlock.wast +++ b/test/async/deadlock.wast @@ -40,11 +40,11 @@ (import "" "waitable.join" (func $waitable.join (param i32 i32))) (import "" "waitable-set.new" (func $waitable-set.new (result i32))) (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) - (import "" "f" (func $f (param i32 i32) (result i32))) + (import "" "f" (func $f (param i32) (result i32))) (func (export "g") (result i32) (local $ws i32) (local $ret i32) (local $subtaski i32) - (local.set $ret (call $f (i32.const 0) (i32.const 0))) + (local.set $ret (call $f (i32.const 0))) (local.set $subtaski (i32.shr_u (local.get $ret) (i32.const 4))) (local.set $ws (call $waitable-set.new)) (call $waitable.join (local.get $subtaski) (local.get $ws)) diff --git a/test/async/drop-subtask.wast b/test/async/drop-subtask.wast index 1959866b..ac4b988c 100644 --- a/test/async/drop-subtask.wast +++ b/test/async/drop-subtask.wast @@ -62,14 +62,14 @@ (import "" "waitable.join" (func $waitable.join (param i32 i32))) (import "" "waitable-set.new" (func $waitable-set.new (result i32))) (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) - (import "" "loop" (func $loop (param i32 i32) (result i32))) + (import "" "loop" (func $loop (result i32))) (import "" "return" (func $return)) (func $drop-after-return (export "drop-after-return") (result i32) (local $ret i32) (local $ws i32) (local $subtask i32) ;; start 'loop' - (local.set $ret (call $loop (i32.const 0xdead) (i32.const 0xbeef))) + (local.set $ret (call $loop)) (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) (then unreachable)) (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) diff --git a/test/async/drop-waitable-set.wast b/test/async/drop-waitable-set.wast index cf30223d..bce0af8c 100644 --- a/test/async/drop-waitable-set.wast +++ b/test/async/drop-waitable-set.wast @@ -51,14 +51,14 @@ (core instance $memory (instantiate $Memory)) (core module $Core (import "" "mem" (memory 1)) - (import "" "wait-on-set" (func $wait-on-set (param i32 i32) (result i32))) + (import "" "wait-on-set" (func $wait-on-set (result i32))) (import "" "drop-while-waiting" (func $drop-while-waiting)) (func $run (export "run") (result i32) (local $ret i32) ;; start an async call to 'wait-on-set' which blocks, waiting on a ;; waitable-set. - (local.set $ret (call $wait-on-set (i32.const 0xdeadbeef) (i32.const 0xdeadbeef))) + (local.set $ret (call $wait-on-set)) (if (i32.ne (i32.const 0x11) (local.get $ret)) (then unreachable)) diff --git a/test/async/empty-wait.wast b/test/async/empty-wait.wast index 5ee17505..f034fbfd 100644 --- a/test/async/empty-wait.wast +++ b/test/async/empty-wait.wast @@ -126,8 +126,8 @@ (import "" "waitable-set.new" (func $waitable-set.new (result i32))) (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) (import "" "subtask.drop" (func $subtask.drop (param i32))) - (import "" "blocker" (func $blocker (param i32 i32) (result i32))) - (import "" "unblocker" (func $unblocker (param i32 i32) (result i32))) + (import "" "blocker" (func $blocker (param i32) (result i32))) + (import "" "unblocker" (func $unblocker (param i32) (result i32))) (global $ws (mut i32) (i32.const 0)) (func $start (global.set $ws (call $waitable-set.new))) @@ -140,7 +140,7 @@ ;; call 'blocker'; it should block (local.set $retp1 (i32.const 4)) - (local.set $ret (call $blocker (i32.const 0xdeadbeef) (local.get $retp1))) + (local.set $ret (call $blocker (local.get $retp1))) (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) (then unreachable)) (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) @@ -149,7 +149,7 @@ ;; call 'unblocker' to unblock 'blocker'; it should complete eagerly (local.set $retp2 (i32.const 8)) - (local.set $ret (call $unblocker (i32.const 0xbeefdead) (local.get $retp2))) + (local.set $ret (call $unblocker (local.get $retp2))) (if (i32.ne (i32.const 2 (; RETURNED ;)) (local.get $ret)) (then unreachable)) (if (i32.ne (i32.const 43) (i32.load (local.get $retp2))) diff --git a/test/async/partial-stream-copies.wast b/test/async/partial-stream-copies.wast index 43d7501d..0526c36f 100644 --- a/test/async/partial-stream-copies.wast +++ b/test/async/partial-stream-copies.wast @@ -145,7 +145,7 @@ (import "" "transform" (func $transform (param i32 i32) (result i32))) (func $run (export "run") (result i32) - (local $ret i32) (local $ret64 i64) (local $paramp i32) (local $retp i32) + (local $ret i32) (local $ret64 i64) (local $retp i32) (local $insr i32) (local $insw i32) (local $outsr i32) (local $subtask i32) (local $event_code i32) (local $index i32) (local $payload i32) (local $ws i32) @@ -160,10 +160,8 @@ (then unreachable)) ;; call 'transform' which will return a readable stream $outsr eagerly - (local.set $paramp (i32.const 4)) (local.set $retp (i32.const 8)) - (i32.store (local.get $paramp) (local.get $insr)) - (local.set $ret (call $transform (local.get $paramp) (local.get $retp))) + (local.set $ret (call $transform (local.get $insr) (local.get $retp))) (if (i32.ne (i32.const 2 (; RETURNED=2 | (0<<4) ;)) (local.get $ret)) (then unreachable)) (local.set $outsr (i32.load (local.get $retp))) diff --git a/test/async/trap-on-reenter.wast b/test/async/trap-on-reenter.wast index 90df6405..4c1286df 100644 --- a/test/async/trap-on-reenter.wast +++ b/test/async/trap-on-reenter.wast @@ -24,12 +24,12 @@ (core instance $memory (instantiate $Memory)) (core module $CoreChild - (import "" "a" (func $a (param i32 i32) (result i32))) + (import "" "a" (func $a (result i32))) (func (export "b") (result i32) (i32.const 1 (; YIELD ;)) ) (func (export "b-cb") (param i32 i32 i32) (result i32) - (call $a (i32.const 0xdeadbeef) (i32.const 0xdeadbeef)) + (call $a) unreachable ) ) @@ -45,12 +45,12 @@ (instance $child (instantiate $Child (with "a" (func $a)))) (core module $CoreOuter - (import "" "b" (func $b (param i32 i32) (result i32))) + (import "" "b" (func $b (result i32))) (func $c (export "c") (result i32) (i32.const 1 (; YIELD ;)) ) (func $c-cb (export "c-cb") (param i32 i32 i32) (result i32) - (call $b (i32.const 0xdeadbeef) (i32.const 0xdeadbeef)) + (call $b) ) ) (canon lower (func $child "b") async (memory $core_inner "mem") (core func $b))