Skip to content

Commit

Permalink
Add Symbols.
Browse files Browse the repository at this point in the history
This allows e.g. setting the @@iterator property on an object, turning
it into an Iterable.
  • Loading branch information
tommie committed Oct 14, 2021
1 parent db84fcc commit 8e87249
Show file tree
Hide file tree
Showing 10 changed files with 413 additions and 7 deletions.
31 changes: 31 additions & 0 deletions object.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ func (o *Object) Set(key string, val interface{}) error {
return nil
}

// SetSymbol will set a property on the Object to a given value.
// Supports all value types, eg: Object, Array, Date, Set, Map etc
// If the value passed is a Go supported primitive (string, int32, uint32, int64, uint64, float64, big.Int)
// then a *Value will be created and set as the value property.
func (o *Object) SetSymbol(key *Symbol, val interface{}) error {
value, err := coerceValue(o.ctx.iso, val)
if err != nil {
return err
}

C.ObjectSetAnyKey(o.ptr, key.ptr, value.ptr)
return nil
}

// Set will set a given index on the Object to a given value.
// Supports all value types, eg: Object, Array, Date, Set, Map etc
// If the value passed is a Go supported primitive (string, int32, uint32, int64, uint64, float64, big.Int)
Expand All @@ -92,6 +106,12 @@ func (o *Object) Get(key string) (*Value, error) {
return valueResult(o.ctx, rtn)
}

// GetSymbol tries to get a Value for a given Object property key.
func (o *Object) GetSymbol(key *Symbol) (*Value, error) {
rtn := C.ObjectGetAnyKey(o.ptr, key.ptr)
return valueResult(o.ctx, rtn)
}

// GetIdx tries to get a Value at a give Object index.
func (o *Object) GetIdx(idx uint32) (*Value, error) {
rtn := C.ObjectGetIdx(o.ptr, C.uint32_t(idx))
Expand All @@ -106,6 +126,12 @@ func (o *Object) Has(key string) bool {
return C.ObjectHas(o.ptr, ckey) != 0
}

// HasSymbol calls the abstract operation HasProperty(O, P) described in ECMA-262, 7.3.10.
// Returns true, if the object has the property, either own or on the prototype chain.
func (o *Object) HasSymbol(key *Symbol) bool {
return C.ObjectHasAnyKey(o.ptr, key.ptr) != 0
}

// HasIdx returns true if the object has a value at the given index.
func (o *Object) HasIdx(idx uint32) bool {
return C.ObjectHasIdx(o.ptr, C.uint32_t(idx)) != 0
Expand All @@ -118,6 +144,11 @@ func (o *Object) Delete(key string) bool {
return C.ObjectDelete(o.ptr, ckey) != 0
}

// DeleteSymbol returns true if successful in deleting a named property on the object.
func (o *Object) DeleteSymbol(key *Symbol) bool {
return C.ObjectDeleteAnyKey(o.ptr, key.ptr) != 0
}

// DeleteIdx returns true if successful in deleting a value at a given index of the object.
func (o *Object) DeleteIdx(idx uint32) bool {
return C.ObjectDeleteIdx(o.ptr, C.uint32_t(idx)) != 0
Expand Down
17 changes: 17 additions & 0 deletions object_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ func TestObjectTemplate(t *testing.T) {
}
}

func TestObjectTemplateSetSymbol(t *testing.T) {
t.Parallel()
iso := v8.NewIsolate()
defer iso.Dispose()
obj := v8.NewObjectTemplate(iso)

val, _ := v8.NewValue(iso, "bar")
objVal := v8.NewObjectTemplate(iso)

if err := obj.SetSymbol(v8.SymbolIterator(iso), val); err != nil {
t.Errorf("failed to set property: %v", err)
}
if err := obj.SetSymbol(v8.SymbolIterator(iso), objVal); err != nil {
t.Errorf("failed to set template property: %v", err)
}
}

func TestObjectTemplate_panic_on_nil_isolate(t *testing.T) {
t.Parallel()

Expand Down
29 changes: 26 additions & 3 deletions object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func TestObjectSet(t *testing.T) {
ctx := v8.NewContext()
defer ctx.Isolate().Dispose()
defer ctx.Close()

symIter := v8.SymbolIterator(ctx.Isolate())
val, _ := ctx.RunScript("const foo = {}; foo", "")
obj, _ := val.AsObject()
obj.Set("bar", "baz")
Expand All @@ -62,6 +64,9 @@ func TestObjectSet(t *testing.T) {
if err := obj.Set("a", 0); err == nil {
t.Error("expected error but got <nil>")
}
if err := obj.SetSymbol(symIter, "sym"); err != nil {
t.Errorf("unexpected error: %v", err)
}
if err := obj.SetIdx(10, "ten"); err != nil {
t.Errorf("unexpected error: %v", err)
}
Expand All @@ -79,14 +84,21 @@ func TestObjectGet(t *testing.T) {
ctx := v8.NewContext()
defer ctx.Isolate().Dispose()
defer ctx.Close()
val, _ := ctx.RunScript("const foo = { bar: 'baz'}; foo", "")
symIter := v8.SymbolIterator(ctx.Isolate())
val, err := ctx.RunScript("const foo = { bar: 'baz', [Symbol.iterator]: 'gee'}; foo", "")
if err != nil {
t.Fatalf("RunScript failed: %v", err)
}
obj, _ := val.AsObject()
if bar, _ := obj.Get("bar"); bar.String() != "baz" {
t.Errorf("unexpected value: %q", bar)
}
if baz, _ := obj.Get("baz"); !baz.IsUndefined() {
t.Errorf("unexpected value: %q", baz)
}
if got, _ := obj.GetSymbol(symIter); got.String() != "gee" {
t.Errorf("unexpected value: %q", got)
}
ctx.RunScript("foo[5] = 5", "")
if five, _ := obj.GetIdx(5); five.Integer() != 5 {
t.Errorf("unexpected value: %q", five)
Expand All @@ -102,14 +114,18 @@ func TestObjectHas(t *testing.T) {
ctx := v8.NewContext()
defer ctx.Isolate().Dispose()
defer ctx.Close()
val, _ := ctx.RunScript("const foo = {a: 1, '2': 2}; foo", "")
symIter := v8.SymbolIterator(ctx.Isolate())
val, _ := ctx.RunScript("const foo = {a: 1, '2': 2, [Symbol.iterator]: 3}; foo", "")
obj, _ := val.AsObject()
if !obj.Has("a") {
t.Error("expected true, got false")
}
if obj.Has("c") {
t.Error("expected false, got true")
}
if !obj.HasSymbol(symIter) {
t.Error("expected true, got false")
}
if !obj.HasIdx(2) {
t.Error("expected true, got false")
}
Expand All @@ -124,7 +140,8 @@ func TestObjectDelete(t *testing.T) {
ctx := v8.NewContext()
defer ctx.Isolate().Dispose()
defer ctx.Close()
val, _ := ctx.RunScript("const foo = { bar: 'baz', '2': 2}; foo", "")
symIter := v8.SymbolIterator(ctx.Isolate())
val, _ := ctx.RunScript("const foo = { bar: 'baz', '2': 2, [Symbol.iterator]: 3}; foo", "")
obj, _ := val.AsObject()
if !obj.Has("bar") {
t.Error("expected property to exist")
Expand All @@ -135,6 +152,12 @@ func TestObjectDelete(t *testing.T) {
if obj.Has("bar") {
t.Error("expected property to be deleted")
}
if !obj.DeleteSymbol(symIter) {
t.Error("expected delete to return true, got false")
}
if obj.HasSymbol(symIter) {
t.Error("expected property to be deleted")
}
if !obj.DeleteIdx(2) {
t.Error("expected delete to return true, got false")
}
Expand Down
60 changes: 60 additions & 0 deletions symbol.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2021 Roger Chapman and the v8go contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package v8go

import (
"fmt"
"unsafe"

// #include <stdlib.h>
// #include "v8go.h"
"C"
)

// A Symbol represents a JavaScript symbol (ECMA-262 edition 6).
type Symbol struct {
*Value
}

func SymbolAsyncIterator(iso *Isolate) *Symbol { return symbolByIndex(iso, C.SYMBOL_ASYNC_ITERATOR) }
func SymbolHasInstance(iso *Isolate) *Symbol { return symbolByIndex(iso, C.SYMBOL_HAS_INSTANCE) }
func SymbolIsConcatSpreadable(iso *Isolate) *Symbol {
return symbolByIndex(iso, C.SYMBOL_IS_CONCAT_SPREADABLE)
}
func SymbolIterator(iso *Isolate) *Symbol { return symbolByIndex(iso, C.SYMBOL_ITERATOR) }
func SymbolMatch(iso *Isolate) *Symbol { return symbolByIndex(iso, C.SYMBOL_MATCH) }
func SymbolReplace(iso *Isolate) *Symbol { return symbolByIndex(iso, C.SYMBOL_REPLACE) }
func SymbolSearch(iso *Isolate) *Symbol { return symbolByIndex(iso, C.SYMBOL_SEARCH) }
func SymbolSplit(iso *Isolate) *Symbol { return symbolByIndex(iso, C.SYMBOL_SPLIT) }
func SymbolToPrimitive(iso *Isolate) *Symbol { return symbolByIndex(iso, C.SYMBOL_TO_PRIMITIVE) }
func SymbolToStringTag(iso *Isolate) *Symbol { return symbolByIndex(iso, C.SYMBOL_TO_STRING_TAG) }
func SymbolUnscopables(iso *Isolate) *Symbol { return symbolByIndex(iso, C.SYMBOL_UNSCOPABLES) }

// symbolByIndex is a Go-to-C helper for obtaining builtin symbols.
func symbolByIndex(iso *Isolate, idx C.SymbolIndex) *Symbol {
val := C.BuiltinSymbol(iso.ptr, idx)
if val == nil {
panic(fmt.Errorf("unknown symbol index: %d", idx))
}
return &Symbol{&Value{val, nil}}
}

// Description returns the string representation of the symbol,
// e.g. "Symbol.asyncIterator".
func (sym *Symbol) Description() string {
s := C.SymbolDescription(sym.Value.ptr)
defer C.free(unsafe.Pointer(s))
return C.GoString(s)
}

// String returns Description().
func (sym *Symbol) String() string {
return sym.Description()
}

// value implements Valuer.
func (sym *Symbol) value() *Value {
return sym.Value
}
43 changes: 43 additions & 0 deletions symbol_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2021 Roger Chapman and the v8go contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package v8go_test

import (
"testing"

v8 "rogchap.com/v8go"
)

func TestBuiltinSymbol(t *testing.T) {
t.Parallel()

iso := v8.NewIsolate()
defer iso.Dispose()

tsts := []struct {
Func func(*v8.Isolate) *v8.Symbol
WantDescription string
}{
{v8.SymbolAsyncIterator, "Symbol.asyncIterator"},
{v8.SymbolHasInstance, "Symbol.hasInstance"},
{v8.SymbolIsConcatSpreadable, "Symbol.isConcatSpreadable"},
{v8.SymbolIterator, "Symbol.iterator"},
{v8.SymbolMatch, "Symbol.match"},
{v8.SymbolReplace, "Symbol.replace"},
{v8.SymbolSearch, "Symbol.search"},
{v8.SymbolSplit, "Symbol.split"},
{v8.SymbolToPrimitive, "Symbol.toPrimitive"},
{v8.SymbolToStringTag, "Symbol.toStringTag"},
{v8.SymbolUnscopables, "Symbol.unscopables"},
}
for _, tst := range tsts {
t.Run(tst.WantDescription, func(t *testing.T) {
iter := tst.Func(iso)
if iter.Description() != tst.WantDescription {
t.Errorf("Description: got %q, want %q", iter.Description(), tst.WantDescription)
}
})
}
}
44 changes: 44 additions & 0 deletions template.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,50 @@ func (t *template) Set(name string, val interface{}, attributes ...PropertyAttri
return nil
}

// SetSymbol adds a property to each instance created by this template.
// The property must be defined either as a primitive value, or a template.
// If the value passed is a Go supported primitive (string, int32, uint32, int64, uint64, float64, big.Int)
// then a value will be created and set as the value property.
func (t *template) SetSymbol(key *Symbol, val interface{}, attributes ...PropertyAttribute) error {
var attrs PropertyAttribute
for _, a := range attributes {
attrs |= a
}

switch v := val.(type) {
case string, int32, uint32, int64, uint64, float64, bool, *big.Int:
newVal, err := NewValue(t.iso, v)
if err != nil {
return fmt.Errorf("v8go: unable to create new value: %v", err)
}
if C.TemplateSetAnyValue(t.ptr, key.ptr, newVal.ptr, C.int(attrs)) == 0 {
return fmt.Errorf("v8go: unable to set property for symbol %v", key)
}
case *ObjectTemplate:
if C.TemplateSetAnyTemplate(t.ptr, key.ptr, v.ptr, C.int(attrs)) == 0 {
return fmt.Errorf("v8go: unable to set property for symbol %v", key)
}
runtime.KeepAlive(v)
case *FunctionTemplate:
if C.TemplateSetAnyTemplate(t.ptr, key.ptr, v.ptr, C.int(attrs)) == 0 {
return fmt.Errorf("v8go: unable to set property for symbol %v", key)
}
runtime.KeepAlive(v)
case *Value:
if v.IsObject() || v.IsExternal() {
return errors.New("v8go: unsupported property: value type must be a primitive or use a template")
}
if C.TemplateSetAnyValue(t.ptr, key.ptr, v.ptr, C.int(attrs)) == 0 {
return fmt.Errorf("v8go: unable to set property for symbol %v", key)
}
default:
return fmt.Errorf("v8go: unsupported property type `%T`, must be one of string, int32, uint32, int64, uint64, float64, *big.Int, *v8go.Value, *v8go.ObjectTemplate or *v8go.FunctionTemplate", v)
}
runtime.KeepAlive(t)

return nil
}

func (t *template) finalizer() {
// Using v8::PersistentBase::Reset() wouldn't be thread-safe to do from
// this finalizer goroutine so just free the wrapper and let the template
Expand Down
Loading

0 comments on commit 8e87249

Please sign in to comment.