From 5fdad7c091a22f4fa434c8ad156db31d99f12217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aram=20H=C4=83v=C4=83rneanu?= Date: Thu, 30 Nov 2023 18:14:35 +0100 Subject: [PATCH] cue/interpreter/wasm: add support for Wasm functions that take and return structs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for structs in Wasm functions. A function declares that is uses or returns a struct by referring to a CUE struct (defined elsewhere) in its type signature. The CUE struct is a straightforward translation of the native struct type with native scalars replaced by their CUE counterpats (e.g. u32 in Rust becomes uint32 in CUE, int16_t in C becomes int16 in CUE, etc). The order of fields in the CUE struct must match that of the target language. Only structs that contain scalars or other supported structs are currently supported. Struct fields that are pointers in the target language are not currently supported. This may change. In order to support structs CUE needs access to the memory allocator of the guest program. For this, the guest must export two Wasm functions with the following C type signatures: void* allocate(int n); void deallocate(void *ptr, int n); Allocate returns a Wasm pointer to a buffer of size n. Deallocate takes a Wasm pointer and the size of the buffer it points to and frees it. Updates #2035. Updates #2281. Change-Id: Ic509448226b22d2a54b70d565db6c702b801a839 Signed-off-by: Aram Hăvărneanu --- cue/interpreter/wasm/{abi_c.go => call.go} | 42 ++- cue/interpreter/wasm/doc.go | 54 ++- cue/interpreter/wasm/layout.go | 343 ++++++++++++++++++ cue/interpreter/wasm/runtime.go | 27 +- cue/interpreter/wasm/testdata/def.txtar | 141 ++++++- cue/interpreter/wasm/testdata/eval.txtar | 137 ++++++- cue/interpreter/wasm/testdata/struct.golden | 32 ++ .../wasm/testdata/struct/Cargo.lock | 62 ++++ .../wasm/testdata/struct/Cargo.toml | 23 ++ .../wasm/testdata/struct/struct.cue | 82 +++++ .../wasm/testdata/struct/struct.rs | 115 ++++++ .../wasm/testdata/struct/struct.wasm | Bin 0 -> 14807 bytes 12 files changed, 1041 insertions(+), 17 deletions(-) rename cue/interpreter/wasm/{abi_c.go => call.go} (77%) create mode 100644 cue/interpreter/wasm/layout.go create mode 100644 cue/interpreter/wasm/testdata/struct.golden create mode 100644 cue/interpreter/wasm/testdata/struct/Cargo.lock create mode 100644 cue/interpreter/wasm/testdata/struct/Cargo.toml create mode 100644 cue/interpreter/wasm/testdata/struct/struct.cue create mode 100644 cue/interpreter/wasm/testdata/struct/struct.rs create mode 100755 cue/interpreter/wasm/testdata/struct/struct.wasm diff --git a/cue/interpreter/wasm/abi_c.go b/cue/interpreter/wasm/call.go similarity index 77% rename from cue/interpreter/wasm/abi_c.go rename to cue/interpreter/wasm/call.go index 3750584b25f..6b4d6e1cf62 100644 --- a/cue/interpreter/wasm/abi_c.go +++ b/cue/interpreter/wasm/call.go @@ -119,6 +119,12 @@ func decNumber(typ cue.Value, val uint64) (r any) { panic(fmt.Sprintf("unsupported argument type %v (kind %v)", typ, typ.IncompleteKind())) } +func encBytes(i *instance, b []byte) *memory { + m, _ := i.Alloc(uint32(len(b))) + m.WriteAt(b, 0) + return m +} + // cABIFunc implements the Wasm/System V ABI translation. The named // function, which must be loadable by the instance, and must be of // the specified sig type, will be called by the runtime after its @@ -126,20 +132,52 @@ func decNumber(typ cue.Value, val uint64) (r any) { // call will be then also be converted back into a Go value and handed // to the runtime. func cABIFunc(i *instance, name string, sig []cue.Value) func(*pkg.CallCtxt) { + // Compute the layout of all encountered structs (arguments + // and result) such that we will have it available at the time + // of an actual call. + argsTyp, resTyp := splitLast(sig) + argLayouts := make([]*structLayout, 0, len(argsTyp)) + var retLayout *structLayout + for _, typ := range argsTyp { + switch typ.IncompleteKind() { + case cue.StructKind: + argLayouts = append(argLayouts, structLayoutVal(typ)) + default: + argLayouts = append(argLayouts, nil) + } + } + if resTyp.IncompleteKind() == cue.StructKind { + retLayout = structLayoutVal(resTyp) + } + fn, _ := i.load(name) return func(c *pkg.CallCtxt) { - var args []uint64 argsTyp, resTyp := splitLast(sig) + args := make([]uint64, 0, len(argsTyp)) for k, typ := range argsTyp { switch typ.IncompleteKind() { case cue.BoolKind: args = append(args, encBool(c.Bool(k))) case cue.IntKind, cue.FloatKind, cue.NumberKind: args = append(args, encNumber(typ, c.Value(k))) + case cue.StructKind: + ms := encodeStruct(i, c.Value(k), argLayouts[k]) + defer i.FreeAll(ms) + + args = append(args, uint64(ms[0].ptr)) default: panic(fmt.Sprintf("unsupported argument type %v (kind %v)", typ, typ.IncompleteKind())) } } + + var retMem *memory + if resTyp.IncompleteKind() == cue.StructKind { + retMem, _ = i.Alloc(uint32(retLayout.size)) + // TODO: add support for structs containing pointers. + defer i.Free(retMem) + args = append(args, uint64(retMem.ptr)) + } + if c.Do() { res, err := fn.Call(i.ctx, args...) if err != nil { @@ -151,6 +189,8 @@ func cABIFunc(i *instance, name string, sig []cue.Value) func(*pkg.CallCtxt) { c.Ret = decBool(res[0]) case cue.IntKind, cue.FloatKind, cue.NumberKind: c.Ret = decNumber(resTyp, res[0]) + case cue.StructKind: + c.Ret = decodeStruct(retMem.Bytes(), retLayout) default: panic(fmt.Sprintf("unsupported result type %v (kind %v)", resTyp, resTyp.IncompleteKind())) } diff --git a/cue/interpreter/wasm/doc.go b/cue/interpreter/wasm/doc.go index 8262c89965a..26ff0018de1 100644 --- a/cue/interpreter/wasm/doc.go +++ b/cue/interpreter/wasm/doc.go @@ -43,10 +43,10 @@ // used by the function (see below) while sig indicates the type // signature of the function. The grammar for sig is: // -// list := ident [ { "," ident } ] -// func := "func" "(" [ list ] ")" ":" ident +// list := expr [ { "," expr } ] +// func := "func" "(" [ list ] ")" ":" expr // -// Where ident are all valid CUE identifiers. +// Where each expr is a valid CUE identifier or selector. // // The specific ABI used may restrict the allowable signatures further. // @@ -88,11 +88,22 @@ // // # ABI requirements for Wasm modules // -// Currently only the [C ABI] is supported. Furthermore, only scalar -// data types can be exchanged between CUE and Wasm. That means booleans, -// sized integers, and sized floats. The sig field in the attribute -// refers to these data types by their CUE names, such as bool, uint16, -// float64. +// Currently only the [System V ABI] (also known as the C ABI) is +// supported. Furthermore, only scalar data types and structs containing +// either scalar types or other structs can be exchanged between CUE +// and Wasm. Scalar means booleans, sized integers, and sized floats. +// The sig field in the attribute refers to these data types by their +// CUE names, such as bool, uint16, float64. +// +// Additionally the Wasm module must export two functions with the +// following C type signature: +// +// void* allocate(int n); +// void deallocate(void *ptr, int n); +// +// Allocate returns a Wasm pointer to a buffer of size n. Deallocate +// takes a Wasm pointer and the size of the buffer it points to and +// frees it. // // # How to compile Rust for use in CUE // @@ -116,7 +127,32 @@ // a * b // } // -// [C ABI]: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md +// The following Rust functions can be used to implement allocate and +// deallocate described above: +// +// #[cfg_attr(all(target_arch = "wasm32"), export_name = "allocate")] +// #[no_mangle] +// pub extern "C" fn _allocate(size: u32) -> *mut u8 { +// allocate(size as usize) +// } +// +// fn allocate(size: usize) -> *mut u8 { +// let vec: Vec> = Vec::with_capacity(size); +// +// Box::into_raw(vec.into_boxed_slice()) as *mut u8 +// } +// +// #[cfg_attr(all(target_arch = "wasm32"), export_name = "deallocate")] +// #[no_mangle] +// pub unsafe extern "C" fn _deallocate(ptr: u32, size: u32) { +// deallocate(ptr as *mut u8, size as usize); +// } +// +// unsafe fn deallocate(ptr: *mut u8, size: usize) { +// let _ = Vec::from_raw_parts(ptr, 0, size); +// } +// +// [System V ABI]: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md // [no_std]: https://docs.rust-embedded.org/book/intro/no-std.html // [WASI]: https://wasi.dev // [cargo target]: https://doc.rust-lang.org/cargo/reference/cargo-targets.html diff --git a/cue/interpreter/wasm/layout.go b/cue/interpreter/wasm/layout.go new file mode 100644 index 00000000000..7ad3f4d5a30 --- /dev/null +++ b/cue/interpreter/wasm/layout.go @@ -0,0 +1,343 @@ +// Copyright 2023 CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wasm + +import ( + "encoding/binary" + "fmt" + "math" + + "cuelang.org/go/cue" + "cuelang.org/go/internal/core/adt" +) + +// typ is the type (or kind) of an external type. +type typ int8 + +const ( + typErr typ = iota + typBool + typUint8 + typUint16 + typUint32 + typUint64 + typInt8 + typInt16 + typInt32 + typInt64 + typFloat32 + typFloat64 + typStruct +) + +// field represents a name struct field. +type field struct { + typ + from string // the field name +} + +// positionedField represents a struct field with a known location. +type positionedField struct { + field + offset int // memory offset in the parent struct. + + inner *structLayout // IFF typ==typStruct +} + +// structLayout describes the memory layout of a struct. +type structLayout struct { + fields []positionedField + size int + align int +} + +func sizeof(t typ) int { + switch t { + case typBool, typUint8, typInt8: + return 1 + case typUint16, typInt16: + return 2 + case typUint32, typInt32, typFloat32: + return 4 + case typUint64, typInt64, typFloat64: + return 8 + } + panic("unreachable") +} + +func encodeStruct(i *instance, v cue.Value, l *structLayout) []*memory { + buf := make([]byte, l.size) + ms := make([]*memory, 1, 2) // cap is 2 for strings and bytes. + + buf, ms = encode(i, v, l, buf, ms) + ms[0] = encBytes(i, buf) + return ms +} + +// encodeStruct serializes v into buf according to the layout. +func encode(i *instance, v cue.Value, l *structLayout, buf []byte, ms []*memory) ([]byte, []*memory) { + for _, f := range l.fields { + arg := v.LookupPath(cue.ParsePath(f.from)) + + switch f.typ { + case typBool: + b, _ := arg.Bool() + if b { + buf[f.offset] = 1 + } else { + buf[f.offset] = 0 + } + + case typUint8: + u, _ := arg.Uint64() + buf[f.offset] = byte(u) + case typUint16: + u, _ := arg.Uint64() + binary.LittleEndian.PutUint16(buf[f.offset:], uint16(u)) + case typUint32: + u, _ := arg.Uint64() + binary.LittleEndian.PutUint32(buf[f.offset:], uint32(u)) + case typUint64: + u, _ := arg.Uint64() + binary.LittleEndian.PutUint64(buf[f.offset:], u) + + case typInt8: + u, _ := arg.Int64() + buf[f.offset] = byte(u) + case typInt16: + u, _ := arg.Int64() + binary.LittleEndian.PutUint16(buf[f.offset:], uint16(u)) + case typInt32: + u, _ := arg.Int64() + binary.LittleEndian.PutUint32(buf[f.offset:], uint32(u)) + case typInt64: + u, _ := arg.Int64() + binary.LittleEndian.PutUint64(buf[f.offset:], uint64(u)) + + case typFloat32: + x, _ := arg.Float64() + binary.LittleEndian.PutUint32(buf[f.offset:], math.Float32bits(float32(x))) + case typFloat64: + x, _ := arg.Float64() + binary.LittleEndian.PutUint64(buf[f.offset:], math.Float64bits(x)) + + case typStruct: + encode(i, arg, f.inner, buf[f.offset:], ms) + + default: + panic(fmt.Sprintf("unsupported argument %v (kind %v)", v, v.IncompleteKind())) + } + } + return buf, ms +} + +// decodeStruct takes the binary representation of a struct described +// by the layout and returns its Go representation as a map. +func decodeStruct(buf []byte, l *structLayout) map[string]any { + m := make(map[string]any) + + for _, f := range l.fields { + switch f.typ { + case typBool: + u := buf[f.offset] + if u == 1 { + m[f.from] = true + } else { + m[f.from] = false + } + + case typUint8: + u := buf[f.offset] + m[f.from] = u + case typUint16: + u := binary.LittleEndian.Uint16(buf[f.offset:]) + m[f.from] = u + case typUint32: + u := binary.LittleEndian.Uint32(buf[f.offset:]) + m[f.from] = u + case typUint64: + u := binary.LittleEndian.Uint64(buf[f.offset:]) + m[f.from] = u + + case typInt8: + u := buf[f.offset] + m[f.from] = int8(u) + case typInt16: + u := binary.LittleEndian.Uint16(buf[f.offset:]) + m[f.from] = int16(u) + case typInt32: + u := binary.LittleEndian.Uint32(buf[f.offset:]) + m[f.from] = int32(u) + case typInt64: + u := binary.LittleEndian.Uint64(buf[f.offset:]) + m[f.from] = int64(u) + + case typFloat32: + u := binary.LittleEndian.Uint32(buf[f.offset:]) + m[f.from] = math.Float32frombits(u) + case typFloat64: + u := binary.LittleEndian.Uint64(buf[f.offset:]) + m[f.from] = math.Float64frombits(u) + + case typStruct: + to := f.offset + f.inner.size + m[f.from] = decodeStruct(buf[f.offset:to], f.inner) + + default: + panic(fmt.Sprintf("unsupported argument type: %v", f.typ)) + } + } + return m +} + +func align(x, n int) int { + return (x + n - 1) & ^(n - 1) +} + +// structLayoutVal returns the System V (C ABI) memory layout of the +// struct expressed by t. +func structLayoutVal(t cue.Value) *structLayout { + if t.IncompleteKind() != adt.StructKind { + panic("expected CUE struct") + } + + var sl structLayout + off, size := 0, 0 + for i, _ := t.Fields(cue.Attributes(true)); i.Next(); { + f := i.Value() + path := i.Selector().String() + + switch f.IncompleteKind() { + case adt.StructKind: + inner := structLayoutVal(f) + off = align(off, inner.align) + + lval := positionedField{ + field: field{ + typ: typStruct, + from: path, + }, + offset: off, + inner: inner, + } + sl.fields = append(sl.fields, lval) + + off += inner.size + case cue.BoolKind, cue.IntKind, cue.FloatKind, cue.NumberKind: + typ := typVal(f) + size = sizeof(typ) + off = align(off, size) + + lval := positionedField{ + field: field{ + typ: typ, + from: path, + }, + offset: off, + } + sl.fields = append(sl.fields, lval) + + off += size + default: + panic(fmt.Sprintf("unsupported argument type %v (kind %v)", f, f.IncompleteKind())) + } + } + + // The alignment of a struct is the maximum alignment of its + // constituent fields. + maxalign := 0 + for _, f := range sl.fields { + if f.typ == typStruct { + if f.inner.align > maxalign { + maxalign = f.inner.align + } + continue + } + if sizeof(f.typ) > maxalign { + maxalign = sizeof(f.typ) + } + } + sl.size = align(off, maxalign) + sl.align = maxalign + + return &sl +} + +func typVal(v cue.Value) typ { + switch v.IncompleteKind() { + case cue.BoolKind: + return typBool + case cue.IntKind, cue.FloatKind, cue.NumberKind: + return typNum(v) + default: + panic(fmt.Sprintf("unsupported argument type %v (kind %v)", v, v.IncompleteKind())) + } +} + +func typNum(t cue.Value) typ { + ctx := t.Context() + + _int8 := ctx.CompileString("int8") + if _int8.Subsume(t) == nil { + return typInt8 + } + + _uint8 := ctx.CompileString("uint8") + if _uint8.Subsume(t) == nil { + return typUint8 + } + + _int16 := ctx.CompileString("int16") + if _int16.Subsume(t) == nil { + return typInt16 + } + + _uint16 := ctx.CompileString("uint16") + if _uint16.Subsume(t) == nil { + return typUint16 + } + + _int32 := ctx.CompileString("int32") + if _int32.Subsume(t) == nil { + return typInt32 + } + + _uint32 := ctx.CompileString("uint32") + if _uint32.Subsume(t) == nil { + return typUint32 + } + + _int64 := ctx.CompileString("int64") + if _int64.Subsume(t) == nil { + return typInt64 + } + + _uint64 := ctx.CompileString("uint64") + if _uint64.Subsume(t) == nil { + return typUint64 + } + + _float32 := ctx.CompileString("float32") + if _float32.Subsume(t) == nil { + return typFloat32 + } + + _float64 := ctx.CompileString("float64") + if _float64.Subsume(t) == nil { + return typFloat64 + } + + panic("unreachable") +} diff --git a/cue/interpreter/wasm/runtime.go b/cue/interpreter/wasm/runtime.go index 6c0422b77f4..44957012d2d 100644 --- a/cue/interpreter/wasm/runtime.go +++ b/cue/interpreter/wasm/runtime.go @@ -157,6 +157,13 @@ func (i *instance) Free(m *memory) { i.free.Call(i.ctx, uint64(m.ptr), uint64(m.len)) } +// Free frees several previously allocated guest memories. +func (i *instance) FreeAll(ms []*memory) { + for _, m := range ms { + i.free.Call(i.ctx, uint64(m.ptr), uint64(m.len)) + } +} + // memory is a read and write reference to guest memory that the host // requested. type memory struct { @@ -171,15 +178,25 @@ func (m *memory) Bytes() []byte { if !ok { panic(fmt.Sprintf("can't read %d bytes from Wasm address %#x", m.len, m.ptr)) } - return bytes + return append([]byte{}, bytes...) } -// Write writes into p guest memory referenced by n. -// p must fit into m. -func (m *memory) Write(p []byte) (int, error) { - ok := m.i.instance.Memory().Write(m.ptr, p) +// WriteAt writes p at the given relative offset within m. +// It panics if buf doesn't fit into m, or if off is out of bounds. +func (m *memory) WriteAt(p []byte, off int64) (int, error) { + if (off < 0) || (off >= 1<<32-1) { + panic(fmt.Sprintf("can't write %d bytes to Wasm address %#x", len(p), m.ptr)) + } + + ok := m.i.instance.Memory().Write(m.ptr+uint32(off), p) if !ok { panic(fmt.Sprintf("can't write %d bytes to Wasm address %#x", len(p), m.ptr)) } return len(p), nil } + +// Args returns a memory in the form of pair of arguments directly +// passable to Wasm. +func (m *memory) Args() []uint64 { + return []uint64{uint64(m.ptr), uint64(m.len)} +} diff --git a/cue/interpreter/wasm/testdata/def.txtar b/cue/interpreter/wasm/testdata/def.txtar index bc8a405956a..f63783e6189 100644 --- a/cue/interpreter/wasm/testdata/def.txtar +++ b/cue/interpreter/wasm/testdata/def.txtar @@ -4,7 +4,7 @@ # Since this is `cue def` we don't run the functions, so we see the # call to this higher-order construct. -exec cue def ./basic ./morewasm ./noload ./unusedwasm +exec cue def ./basic ./morewasm ./noload ./struct ./unusedwasm cmp stdout eval.out -- basic/foo.cue -- @@ -73,6 +73,61 @@ add: _ @extern("foo.wasm", abi=c, sig="func(int64, int64): int64") x0: add(1, 2) x1: add(-1, 2) x2: add(100, 1) +-- struct/struct.cue -- +@extern("wasm") +package p + +import "math" + +#vector2: { + x: float64 + y: float64 +} + +magnitude2: _ @extern("struct.wasm", abi=c, sig="func(#vector2): float64") +magnitude3: _ @extern("struct.wasm", abi=c, sig="func(#vector3): float64") + +_v0: {x: 1, y: 1} +_v1: {x: math.Sqrt2, y: math.Sqrt2} +_v2: {x: 123.456, y: 789.012} + +m0: magnitude2(_v0) +m1: magnitude2(_v1) +m2: magnitude2(_v2) + +#vector3: { + x: float64 + y: float64 + z: float64 +} + +_v3: {x: 1, y: 1, z: 1} +_v4: {x: 0, y: 2, z: 2} +_v5: {x: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, y: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, z: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144} + +m3: magnitude3(_v3) +m4: magnitude3(_v4) +m5: magnitude3(_v5) + +cornucopia: _ @extern("struct.wasm", abi=c, sig="func(#cornucopia): int64") + +#cornucopia: { + b: bool + n0: int16 + n1: uint8, + n2: int64 +} + +_c0: {b: true, n0: 10, n1: 20, n2: 30} +_c1: {b: false, n0: 1, n1: 2, n2: 3} +_c2: {b: false, n0: -1, n1: 0, n2: 100} +_c3: {b: false, n0: -15000, n1: 10, n2: -10000000} + +c0: cornucopia(_c0) +c1: cornucopia(_c1) +c2: cornucopia(_c2) +c3: cornucopia(_c3) +-- struct/struct.wasm -- -- unusedwasm/foo.wasm -- -- unusedwasm/empty.wasm -- -- eval.out -- @@ -116,6 +171,90 @@ x: 42 // --- package p +import "math" + +#vector2: { + x: float64 + y: float64 +} +magnitude2: magnitude2() @extern("struct.wasm", abi=c, sig="func(#vector2): float64") +magnitude3: magnitude3() @extern("struct.wasm", abi=c, sig="func(#vector3): float64") +_v0: { + x: 1 + y: 1 +} +_v1: { + x: math.Sqrt2 + y: math.Sqrt2 +} +_v2: { + x: 123.456 + y: 789.012 +} +m0: magnitude2(_v0) +m1: magnitude2(_v1) +m2: magnitude2(_v2) +#vector3: { + x: float64 + y: float64 + z: float64 +} +_v3: { + x: 1 + y: 1 + z: 1 +} +_v4: { + x: 0 + y: 2 + z: 2 +} +_v5: { + x: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 + y: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 + z: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 +} +m3: magnitude3(_v3) +m4: magnitude3(_v4) +m5: magnitude3(_v5) +cornucopia: cornucopia() @extern("struct.wasm", abi=c, sig="func(#cornucopia): int64") +#cornucopia: { + b: bool + n0: int16 + n1: uint8 + n2: int64 +} +_c0: { + b: true + n0: 10 + n1: 20 + n2: 30 +} +_c1: { + b: false + n0: 1 + n1: 2 + n2: 3 +} +_c2: { + b: false + n0: -1 + n1: 0 + n2: 100 +} +_c3: { + b: false + n0: -15000 + n1: 10 + n2: -10000000 +} +c0: cornucopia(_c0) +c1: cornucopia(_c1) +c2: cornucopia(_c2) +c3: cornucopia(_c3) +// --- +package p + add: add() @extern("foo.wasm", abi=c, sig="func(int64, int64): int64") x0: add(1, 2) x1: add(-1, 2) diff --git a/cue/interpreter/wasm/testdata/eval.txtar b/cue/interpreter/wasm/testdata/eval.txtar index 5b75c437c67..8719f47ce4b 100644 --- a/cue/interpreter/wasm/testdata/eval.txtar +++ b/cue/interpreter/wasm/testdata/eval.txtar @@ -1,4 +1,4 @@ -exec cue eval -a ./basic ./morewasm ./noload ./unusedwasm +exec cue eval -a ./basic ./morewasm ./noload ./struct ./unusedwasm cmp stdout eval.out -- basic/foo.cue -- @@ -67,6 +67,61 @@ add: _ @extern("foo.wasm", abi=c, sig="func(int64, int64): int64") x0: add(1, 2) x1: add(-1, 2) x2: add(100, 1) +-- struct/struct.cue -- +@extern("wasm") +package p + +import "math" + +#vector2: { + x: float64 + y: float64 +} + +magnitude2: _ @extern("struct.wasm", abi=c, sig="func(#vector2): float64") +magnitude3: _ @extern("struct.wasm", abi=c, sig="func(#vector3): float64") + +_v0: {x: 1, y: 1} +_v1: {x: math.Sqrt2, y: math.Sqrt2} +_v2: {x: 123.456, y: 789.012} + +m0: magnitude2(_v0) +m1: magnitude2(_v1) +m2: magnitude2(_v2) + +#vector3: { + x: float64 + y: float64 + z: float64 +} + +_v3: {x: 1, y: 1, z: 1} +_v4: {x: 0, y: 2, z: 2} +_v5: {x: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, y: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, z: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144} + +m3: magnitude3(_v3) +m4: magnitude3(_v4) +m5: magnitude3(_v5) + +cornucopia: _ @extern("struct.wasm", abi=c, sig="func(#cornucopia): int64") + +#cornucopia: { + b: bool + n0: int16 + n1: uint8, + n2: int64 +} + +_c0: {b: true, n0: 10, n1: 20, n2: 30} +_c1: {b: false, n0: 1, n1: 2, n2: 3} +_c2: {b: false, n0: -1, n1: 0, n2: 100} +_c3: {b: false, n0: -15000, n1: 10, n2: -10000000} + +c0: cornucopia(_c0) +c1: cornucopia(_c1) +c2: cornucopia(_c2) +c3: cornucopia(_c3) +-- struct/struct.wasm -- -- unusedwasm/foo.wasm -- -- unusedwasm/empty.wasm -- -- eval.out -- @@ -102,6 +157,86 @@ z: false // --- x: 42 // --- +#vector2: { + x: float64 + y: float64 +} +magnitude2: magnitude2() +magnitude3: magnitude3() +_v0: { + x: 1 + y: 1 +} +_v1: { + x: 1.41421356237309504880168872420969807856967187537694807317667974 + y: 1.41421356237309504880168872420969807856967187537694807317667974 +} +_v2: { + x: 123.456 + y: 789.012 +} +m0: 1.4142135623730951 +m1: 2.0 +m2: 798.6121211702211 +#vector3: { + x: float64 + y: float64 + z: float64 +} +_v3: { + x: 1 + y: 1 + z: 1 +} +_v4: { + x: 0 + y: 2 + z: 2 +} +_v5: { + x: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 + y: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 + z: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 +} +m3: 1.7320508075688772 +m4: 2.8284271247461903 +m5: 6.666666666666667 +cornucopia: cornucopia() +#cornucopia: { + b: bool + n0: int16 + n1: uint8 + n2: int64 +} +_c0: { + b: true + n0: 10 + n1: 20 + n2: 30 +} +_c1: { + b: false + n0: 1 + n1: 2 + n2: 3 +} +_c2: { + b: false + n0: -1 + n1: 0 + n2: 100 +} +_c3: { + b: false + n0: -15000 + n1: 10 + n2: -10000000 +} +c0: 42 +c1: 6 +c2: 99 +c3: -10014990 +// --- add: add x0: 3 x1: 1 diff --git a/cue/interpreter/wasm/testdata/struct.golden b/cue/interpreter/wasm/testdata/struct.golden new file mode 100644 index 00000000000..7c8bcc87670 --- /dev/null +++ b/cue/interpreter/wasm/testdata/struct.golden @@ -0,0 +1,32 @@ +{ + magnitude2: magnitude2() @extern("struct.wasm", abi=c, sig="func(#vector2): float64") + magnitude3: magnitude3() @extern("struct.wasm", abi=c, sig="func(#vector3): float64") + m0: 1.4142135623730951 + m1: 2.0 + m2: 798.6121211702211 + normalize2: normalize2 @extern("struct.wasm", abi=c, sig="func(#vector2): #vector2") + n2: { + x: 0.7071067811865476 + y: 0.7071067811865476 + } + n2m: 1.0 + m3: 1.7320508075688772 + m4: 2.8284271247461903 + m5: 6.666666666666667 + double3: double3 @extern("struct.wasm", abi=c, sig="func(#vector3): #vector3") + d4: { + x: 0.0 + y: 4.0 + z: 4.0 + } + cornucopia: cornucopia() @extern("struct.wasm", abi=c, sig="func(#cornucopia): int64") + c0: 42 + c1: 6 + c2: 99 + c3: -10014990 + mag: magnitude_foo() @extern("struct.wasm", abi=c, name=magnitude_foo, sig="func(#foo): float64") + mb0: 1.4142135623730951 + mb1: 5.0 + mb2: 37.0 + mb3: 6.472356603278283 +} \ No newline at end of file diff --git a/cue/interpreter/wasm/testdata/struct/Cargo.lock b/cue/interpreter/wasm/testdata/struct/Cargo.lock new file mode 100644 index 00000000000..1d975707b19 --- /dev/null +++ b/cue/interpreter/wasm/testdata/struct/Cargo.lock @@ -0,0 +1,62 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "greet" +version = "0.1.0" +dependencies = [ + "wee_alloc", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/cue/interpreter/wasm/testdata/struct/Cargo.toml b/cue/interpreter/wasm/testdata/struct/Cargo.toml new file mode 100644 index 00000000000..9d5785f6f78 --- /dev/null +++ b/cue/interpreter/wasm/testdata/struct/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "greet" +version = "0.1.0" +edition = "2021" + +[lib] +# cdylib builds a a %.wasm file with `cargo build --release --target wasm32-unknown-unknown` +crate-type = ["cdylib"] +name = "struct" +path = "struct.rs" + +[dependencies] +# wee_aloc is a WebAssembly optimized allocator, which is needed to use non-numeric types like strings. +# See https://docs.rs/wee_alloc/latest/wee_alloc/ +wee_alloc = "0.4.5" + +# Below settings dramatically reduce wasm output size +# See https://rustwasm.github.io/book/reference/code-size.html#optimizing-builds-for-code-sizewasm-opt -Oz -o +# See https://doc.rust-lang.org/cargo/reference/profiles.html#codegen-units +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 \ No newline at end of file diff --git a/cue/interpreter/wasm/testdata/struct/struct.cue b/cue/interpreter/wasm/testdata/struct/struct.cue new file mode 100644 index 00000000000..9e539c38107 --- /dev/null +++ b/cue/interpreter/wasm/testdata/struct/struct.cue @@ -0,0 +1,82 @@ +@extern("wasm") +package p + +import "math" + +#vector2: { + x: float64 + y: float64 +} + +magnitude2: _ @extern("struct.wasm", abi=c, sig="func(#vector2): float64") +magnitude3: _ @extern("struct.wasm", abi=c, sig="func(#vector3): float64") + +_v0: {x: 1, y: 1} +_v1: {x: math.Sqrt2, y: math.Sqrt2} +_v2: {x: 123.456, y: 789.012} + +m0: magnitude2(_v0) +m1: magnitude2(_v1) +m2: magnitude2(_v2) + +normalize2: _ @extern("struct.wasm", abi=c, sig="func(#vector2): #vector2") +n2: normalize2(_v1) +n2m: magnitude2(n2) + +#vector3: { + x: float64 + y: float64 + z: float64 +} + +_v3: {x: 1, y: 1, z: 1} +_v4: {x: 0, y: 2, z: 2} +_v5: {x: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, y: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, z: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144} + +m3: magnitude3(_v3) +m4: magnitude3(_v4) +m5: magnitude3(_v5) + +double3: _ @extern("struct.wasm", abi=c, sig="func(#vector3): #vector3") +d4: double3(_v4) + +cornucopia: _ @extern("struct.wasm", abi=c, sig="func(#cornucopia): int64") + +#cornucopia: { + b: bool + n0: int16 + n1: uint8 + n2: int64 +} + +_c0: {b: true, n0: 10, n1: 20, n2: 30} +_c1: {b: false, n0: 1, n1: 2, n2: 3} +_c2: {b: false, n0: -1, n1: 0, n2: 100} +_c3: {b: false, n0: -15000, n1: 10, n2: -10000000} + +c0: cornucopia(_c0) +c1: cornucopia(_c1) +c2: cornucopia(_c2) +c3: cornucopia(_c3) + +#foo: { + b: bool + bar: #bar +} + +#bar: { + b: bool + baz: #baz + n: uint16 +} + +#baz: { + vec: #vector2 +} + +mag: _ @extern("struct.wasm", abi=c, name=magnitude_foo, sig="func(#foo): float64") + +mb0: mag({b: false, bar: {b: true, baz: {vec: {x: 1, y: 1}}, n: 0}}) +mb1: mag({b: true, bar: {b: false, baz: {vec: {x: 3, y: 4}}, n: 1}}) +mb2: mag({b: false, bar: {b: true, baz: {vec: {x: 12, y: 35}}, n: 5}}) +mb3: mag({b: false, bar: {b: false, baz: {vec: {x: 3.33, y: 5.55}}, n: 110}}) diff --git a/cue/interpreter/wasm/testdata/struct/struct.rs b/cue/interpreter/wasm/testdata/struct/struct.rs new file mode 100644 index 00000000000..4f1a7061824 --- /dev/null +++ b/cue/interpreter/wasm/testdata/struct/struct.rs @@ -0,0 +1,115 @@ +/* + cargo build --release --target wasm32-unknown-unknown && cp target/wasm32-unknown-unknown/release/struct.wasm . +*/ + +extern crate alloc; +extern crate core; +extern crate wee_alloc; + +#[repr(C)] +pub struct Vector2 { + x: f64, + y: f64, +} + +#[repr(C)] +pub struct Vector3 { + x: f64, + y: f64, + z: f64, +} + +#[no_mangle] +pub extern "C" fn magnitude2(v: &Vector2) -> f64 { + (v.x.powi(2) + v.y.powi(2)).sqrt() +} + +#[no_mangle] +pub extern "C" fn magnitude3(v: &Vector3) -> f64 { + (v.x.powi(2) + v.y.powi(2) + v.z.powi(2)).sqrt() +} + +#[no_mangle] +pub extern "C" fn normalize2(v: &Vector2) -> Vector2 { + let l = magnitude2(v); + Vector2 { + x: v.x / l, + y: v.y / l, + } +} + +#[no_mangle] +pub extern "C" fn double3(v: &Vector3) -> Vector3 { + Vector3 { + x: v.x * 2.0, + y: v.y * 2.0, + z: v.z * 2.0, + } +} + +#[repr(C)] +pub struct Cornucopia { + b: bool, + n0: i16, + n1: u8, + n2: i64, +} + +#[no_mangle] +pub extern "C" fn cornucopia(x: &Cornucopia) -> i64 { + if x.b { + return 42; + } + return x.n0 as i64 + x.n1 as i64 + x.n2; +} + +#[repr(C)] +pub struct Foo { + b: bool, + bar: Bar, +} + +#[repr(C)] +pub struct Bar { + b: bool, + baz: Baz, + n: u16, +} + +#[repr(C)] +pub struct Baz { + vec: Vector2, +} + +#[no_mangle] +pub extern "C" fn magnitude_foo(x: &Foo) -> f64 { + magnitude2(&x.bar.baz.vec) +} + +use alloc::vec::Vec; +use std::mem::MaybeUninit; + +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[cfg_attr(all(target_arch = "wasm32"), export_name = "allocate")] +#[no_mangle] +pub extern "C" fn _allocate(size: u32) -> *mut u8 { + allocate(size as usize) +} + +fn allocate(size: usize) -> *mut u8 { + let vec: Vec> = Vec::with_capacity(size); + + Box::into_raw(vec.into_boxed_slice()) as *mut u8 +} + +#[cfg_attr(all(target_arch = "wasm32"), export_name = "deallocate")] +#[no_mangle] +pub unsafe extern "C" fn _deallocate(ptr: u32, size: u32) { + deallocate(ptr as *mut u8, size as usize); +} + +unsafe fn deallocate(ptr: *mut u8, size: usize) { + let _ = Vec::from_raw_parts(ptr, 0, size); +} diff --git a/cue/interpreter/wasm/testdata/struct/struct.wasm b/cue/interpreter/wasm/testdata/struct/struct.wasm new file mode 100755 index 0000000000000000000000000000000000000000..96bc67387f9b9e2881fc65c1678e0de22901397c GIT binary patch literal 14807 zcmd5@3y>T~dG4Os-P^srJMBq2y+!8E!pPWK?Ywq&&O+824ug>&F@A_ZSk2DNp7wON zd)nQTEL-T#n8Y{^Whcf!3}qW9JRBZnNJ6y8r(BfA!yg_x!!fy!IStjBz7#II)NiJFF}&^2NhEx}Bmrqw49y zjGpXplJL;1j%%u#NF-H-CsK*Tz(68}M}no(0|SgJTp3`AB%kNmY*OWHG__S-Rw{wl@hV{>P4p1q3us9Tj@82~|@c))Y;7_@M_Ul)rScPkK`v(vGlL+ zQG#A;$ez8rC%ATWf<+p!^2V9VnWNB{axw}lKe_}fk-1SMgB2C{8Cm@?YRKJ1oLF`v zBW;B-Zw$SojJX5{@pAn%`m4Z&WNVRxm>UN(%@J?tmc zTIR>7R_2pYU3C1BY(h6#06jqG5>vDq^RW zp~;dN=cK)gi|m=6$cVucwk@AJHZcfd>6wY_Q7!%Q;UtiGn%Yx{Tx9dd7_F(i2jsvHy6 z6%?iWV5(^uq3SpvVp(F5Ex%4!h1dcqlDSMWlSR@qG`^{IaZl=XC+9SL6os(1kxYs- z3RD$Fp(;4RZ-h~RN0JLGP#-L?vUdV=1HDy3$0|<}z=lLoaHoD4ov4dO7E&QAIX;#Y zk~)k32J{6vnk3i)sAaW-?Gwj}1_{(xY%kxjl%TaUcl-cFjKwR&6jN#3<${<)H#yfJ zG5RkpojTT=Z5Rgdl}uIQm}KRpL^c7N=yiR() zf>=QVo6drQoCu(%Vg8e5+3#@<CPelmo+^Rx;kXjGd=Lx+?eI18oqDq|Il0g{ z(embZUvyY&HZ)H=e08%C9@dU~vkPG?Lc0?P0p`6%-9K7yOhW{Ad&K8@@Od8S5-*}p zR|?kvE;O+;F8UDv46ebb%0{`T^%qkKK+ zC@xn=phccFM%0nX?4^h%V3G&+`qY6fG4QgjckrzcVkgr#P|Kn0tFku%F|~^54Vxib zml^^GMC?^B+e(1U3i8>A{NPG>F>c8c;~O@|Y=`n}xqR5?t^k3}NK8&D)+M%XC)ab4 z7D>_~2kJ3Es5ROL5E$$G6c>OEG40qw!af_jnkJ>DV_+2s5Nf}gq#U?J_;Au!7dpV zRrWtajC}~cbv3HWm2fP@qf#9=^#J_KqS}ivm+*K8HGdAbMBF3sL&hG|L3sl0d`wB& zczy=tF2GZL(S^9PP1J!3ERmAD1Mq$Tb>dB*x8ah!KS>-uK-I@^8@K>Y`v$7Lkf#sA z;jf|Q3QxP32uoDH8#kt0jC*Dblk*^I5J`D(5+b}B6>cRb@z{g57f|)U2pjxsR9+(y z*@H)cNKE<7YBoc}@ZpQ=Ky(SEp+5pHFxbEkQGlw?VkX`TtEkU%ri=kXjcoXhXdOo( zMr#4>`#?*)5f~n!Ewg7*+LL_;W?62T`(c^SLX6llk5k_n0ECVs15t;V&wHq4NLAN; z1cr(mm0@*j?vXwKzYy^xQH6{XeKI!C_b-V2xH9?#p43k9M7dsBxBj!J%;F+f0^oT} zA=C}q!2cyE#QbMar#PhCpsfEH(8p0nm9P`no3ur?68~Xqx{Z)NENcyq+(Y@Q_7+*K`rak}(KK8YK0EA!vu|7bb z0+5=#O*sq}TZe!HkM9o4n;`Uip;_|X$$y1No6)SGIr#zML-(PJ@}Jb1d`!I+7!DkC zD9!nR1B^?l@?)+JfQkbuqT!p=K$Rb(H>^*$uhFedx1B_C7j6SzL#MZ*kljFdfIdu3 zz4ndZ_BWUxBVQ2hGF*M05Wj?*`c=reKr{ZYkoRKBjRb4K1up{Y)K2nIk8tY*KY)Qa ziNe6U0cXmJn0Fe!eiPv8{kTP9e2VHfLz5@LB7M&gS8Rg34NUJsA^offUCOtp!+#J9 z1-RdXIWoV(?)EY8TMs!hUqOXT`4wt;J1UPusb@g)%xECGL`1g&^Oxv4cteset1#sm zYQGNDh|lodDqmJX<33y%O}cNBi@=i@cybyFb;^D;C?AF@+y4Qem!QUP;A{s$_&8aV z{ffssoR-&^4%<(F8TrT$a8c-qKQ!syjO*>V-izx|Tr?fxPu%uM)ONn%voO7&=)dF=|=vw2)V4qFXxRuXM3_?>2pRm16eqatY> z@fyKw2xQvoR%jc-uBGD3sRZKaI5*d9u%_5OGr5UoCy<_INDMPSoSkJarW<8*XIX2Z z-H9llsCSN3jx?J`S*_lvw~vHD%#vWO6C^9U?gWf+RC1?{HY<0Ub9MiQHj>E@-fx9S z{W2tyL$GVNz3GtEnk_%9G@8f?`$s!1&kq^)>yoq73_JXKBY<@qm8e(?JN}W17X++x z1SA6{UNWFx!|xNBRVqUMcE@W)S};$@!=hIaR{4XgO>lFCxkzALmzEQQqq}RUYLqIw z7M$uXaf>_w<$wUeF5Nj|xK-P;LbqtTRior;Fd~OAF@U27o~!_-e-8XYcBLJ`USgS`G!$6tG49?J!VDe z6S^&(V%NdzcU`~hitBe3b_&v{2pf$uIIGE`B@A;iRe~!->{9K6st7^qI+hU>t4_hG6~k)ibk*&-i|(#5$It4B ze!&T=7pjGl7uuFp zD3pR?PnolgofVCnu3H$nQqlEvy;`#+0m-%BzYEgODF%@3s~*I(^rBJH0~qowgWVQ` zT{B9qW7s8#Y&(9T*tPq)is}Tehq+$x-C_YQWB0g5v$Y^&2}NP|+Bgaug|&h>lrT$< z8Jd^@x)DH|K5`Z<5^fdK<7lPYycxKTQN--<1J5rOgTR-#%N;cyXc5UuaqK;-)7XB^ z@(V!;b}1EV?pZ0X86zQ~uiIYWyH?3Ei#5mT)5b*9bGvI?QLb+2;bQvK45Q%Jsz$*! zBhJN&oTLjzVVBq5MGp$4`8-zMe7@W`Ng>MVoSY9Ub#jHG?Rd6X2wb;lSYe@$?^<5l z7j;6XWI2xOdKO}FP_lZXX=3FyouJj6$AVfBQHo@eVnCUEeyY`Kwnz{f8oLx4dcm;u zYB3CpeRR&`6?&n!yn%>Os$vl_J=gYh!wb!xFlQuETD_Q(L`BzkUB9Z^A*NJS5<3@X zL0{kYty0bNE!S~9ztrn{wn@?-i$mYk;oEM(tvZHNGtNyLC)y!Eioo)9(=$Q~t7FlK z2#^MPp|_`TyBi3j7=<;QI#=39u;uD)EyjPTl4L}cRb&YdW!%>eqx_!)jPC^l?nm8sZ=WZ4%DvdJ)M{PzuVm+ zqEo^43k3|fAJ)98WfZ2ETzmIjNX|An31^#}>`tBiY?)8)>%$Qaxu4O>yAO&@PKroG z8=T3>mtnuQKiUXU{M@%dyR-caOW?k`EJo8QqA2=CB*d0-ax&fvPfo5C%qAzVZeHu1 zoNao+eipCE5iwePCnss~-M3pXzC^1wo3j_~zeHOpsEnQ>ei0wJc?9oh(}nL89LE;? z5>4tYcQ2EZa`&=I>t)2Q!nU?dTk-w-85V;Tk_byI7mkTxU5OWMSyqC(fqLKWi_V+= zYwSeVFa(Qum4#-W6!=N&7rYrDJl(u{&(?$M1s1?!@V82jePy6C-f zexcg%5a_1y&Piz(#!lh75Em)TE=B>9o)z%y&U;1)-)`7oQ>e-JpDR&{-)ycydDU(c zFvebtS9?XU*@jY=6|s;yi?>GNac#V3!J#%=``z=UXo%nBu0y%EiZKsWK~TI1*5Vqy z^su|*8v6*9@MillJkJ)+z6u zj>6so`1g@AATz?-#DMjbCY$zN7uYPSYfL%zDBy8aqol$GAS#DK<_IY96vsbCnZ-4N z0jmSv<0W2$v+;8(p>lKleJ?n;Ra64D;>HF=H2^4AX-dC-mC%}o@=8yhR36u-1F z%HIC2uML!PruUNH)wPRq$mhw1Yq-=JO98Dfl*v91MmcweEii-wvlGOaGF7Kg5IZzR ziFlC!dWXP4`sbYlk;$lcqZlQmD1(27&CtitEaELCXXQqMeF{anmtb@8Ne9?vy*Q=dTt2QjiB_rADGUJdIn=kt(gM-O z87zp>ht^p7NC?$vDWs2t#8JHmiA51e?-`8Gd1vd5@P5D&Y#w)ff#Vk=!}K6(xK@7& zV)YJ(d?Vg2#oHd(w)SzB$1YN-{I3zW;Q(lij=j<^SA(%&uL6=HQa)PnNk*`i!$v!bNP$Qda(}$Q zOQZ;f@h6oio3ENuqx3PTGoo%*Ucm7cKh6_sk`Dssj|t~TDpxoG6M#_8U%i9#WNPrU z%0@r|qx`@6bH)=%GR|kXFwP6rPn=G_pFrvp{L8{xpE&)1l0%na;3wGEY2co7B>Vi3 z_KRIJM0?L}J+|p{n6Qf%?^*j+=rt|73Mi7@tDrQ)VqhQXy%ab1=JR_O8tsModA#|A z!Co0cS7Jx2b`0+7K~pmZ_9!Fb610D`o2_5{XpqS$Cq8RupZ5uBd&P;l_YlB488X-J?x#kiu93#srB z07LSaZ@^;`>ZekjYc0e^P zX_~fggte)3{k$@Aq8~_5D#|mED)-%*Zs9H1@~1K-u==%54F>yucabgpe~+AoM#Z~B z|AE`^6)}DdO0C|)DRiw)oP6Ys>@WxnN;Mfyacc8Rm-Rt9 zwLX0WslB#dx;id)kNJu%{P^j(IAg381KUGt*3!MOhBMbK*MUwqjf>g1GTq-*CZ9j( zwWei^iR>pc|E}veNQV^--!0fkY)x%F0h&%><$$c}-gmaq$K~A3?-_-HjV!-mIAOtZ zys2%C^T=2?=Wy&nOK*i-34@@ALCLU+fo|ze!NO7J_S?wy&X=a9ft>xqY*3jCVP~YM zXK*gufCrGrksNJu1j{PgrJ7|Kr4kN0i&Hz6e!K3!+6>5Gx*-nHL%rk_keZy@dGBvm zgmW@Xt!QHLDj5aO)&rwhntEQ)@7XvnJcDPSL#&X#j+144VIlC`Qs`Qy?@j&Qjo{Lo z@ui!hRTzhv^5=jR`ufb-j~}gCXq#21R;c30+O(zy)^oP5f-k_(;pco)95-YJIlcnj zYSJ$Y*;Sc&loouXUX?UuWEb$Q5}vpY32QclCLiB334=Y=lRQZ)g1_^O$ys*EI(%CM mFS?al=ymWVpSE)Gy19jphu=W%ksF8h#p!l^y0Is`x$}RC+nqcB literal 0 HcmV?d00001