Skip to content

Commit 93b99d9

Browse files
committed
WIP fixes for internalize and zero val translation of undefined and null
1 parent 2b1d432 commit 93b99d9

File tree

4 files changed

+289
-35
lines changed

4 files changed

+289
-35
lines changed

compiler/expressions.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,8 +1213,6 @@ func (c *funcContext) internalize(s *expression, t types.Type) *expression {
12131213
switch u := t.Underlying().(type) {
12141214
case *types.Basic:
12151215
switch {
1216-
case isBoolean(u):
1217-
return c.formatExpr("!!(%s)", s)
12181216
case isInteger(u) && !is64Bit(u):
12191217
return c.fixNumber(c.formatExpr("$parseInt(%s)", s), u)
12201218
case isFloat(u):

compiler/prelude/jsmapping.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,13 @@ var $internalize = function(v, t, recv) {
200200
}
201201
switch (t.kind) {
202202
case $kindBool:
203-
return !!v;
203+
if (v === undefined || v == null) {
204+
return false;
205+
}
206+
if (v.constructor !== Boolean) {
207+
$throwRuntimeError("tried to internalize non-bool value of type " + $typeof(v));
208+
}
209+
return Boolean(v);
204210
case $kindInt:
205211
return parseInt(v);
206212
case $kindInt8:
@@ -259,12 +265,9 @@ var $internalize = function(v, t, recv) {
259265
if (t.methods.length !== 0) {
260266
$throwRuntimeError("cannot internalize " + t.string);
261267
}
262-
if (v === null) {
268+
if (v === null || v === undefined) {
263269
return $ifaceNil;
264270
}
265-
if (v === undefined) {
266-
return new $jsObjectPtr(undefined);
267-
}
268271
switch (v.constructor) {
269272
case Int8Array:
270273
return new ($sliceType($Int8))(v);
@@ -321,6 +324,12 @@ var $internalize = function(v, t, recv) {
321324
case $kindSlice:
322325
return new t($mapArray(v, function(e) { return $internalize(e, t.elem); }));
323326
case $kindString:
327+
if (v === undefined || v == null) {
328+
return "";
329+
}
330+
if (v.constructor !== String) {
331+
$throwRuntimeError("tried to internalize non-string value of type " + $typeof(v));
332+
}
324333
v = String(v);
325334
if ($isASCII(v)) {
326335
return v;
@@ -373,6 +382,33 @@ var $internalize = function(v, t, recv) {
373382
$throwRuntimeError("cannot internalize " + t.string);
374383
};
375384
385+
var $typeof = function(v) {
386+
if (v === undefined) {
387+
return "undefined";
388+
}
389+
390+
if (v === null) {
391+
return "null";
392+
}
393+
394+
var to = typeof v;
395+
396+
switch (to) {
397+
case "boolean":
398+
return to;
399+
case "number":
400+
return to;
401+
case "string":
402+
return to;
403+
case "symbol":
404+
return to;
405+
case "function":
406+
return to;
407+
default:
408+
return v.constructor.name;
409+
}
410+
}
411+
376412
/* $isASCII reports whether string s contains only ASCII characters. */
377413
var $isASCII = function(s) {
378414
for (var i = 0; i < s.length; i++) {

js/js.go

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,47 @@
1-
// Package js provides functions for interacting with native JavaScript APIs. Calls to these functions are treated specially by GopherJS and translated directly to their corresponding JavaScript syntax.
1+
// Package js provides functions for interacting with native JavaScript APIs.
2+
// Calls to these functions are treated specially by GopherJS and translated
3+
// directly to their corresponding JavaScript syntax.
24
//
3-
// Use MakeWrapper to expose methods to JavaScript. When passing values directly, the following type conversions are performed:
5+
// Use MakeWrapper to expose methods to JavaScript.
46
//
5-
// | Go type | JavaScript type | Conversions back to interface{} |
6-
// | --------------------- | --------------------- | ------------------------------- |
7-
// | bool | Boolean | bool |
8-
// | integers and floats | Number | float64 |
9-
// | string | String | string |
10-
// | []int8 | Int8Array | []int8 |
11-
// | []int16 | Int16Array | []int16 |
12-
// | []int32, []int | Int32Array | []int |
13-
// | []uint8 | Uint8Array | []uint8 |
14-
// | []uint16 | Uint16Array | []uint16 |
15-
// | []uint32, []uint | Uint32Array | []uint |
16-
// | []float32 | Float32Array | []float32 |
17-
// | []float64 | Float64Array | []float64 |
18-
// | all other slices | Array | []interface{} |
19-
// | arrays | see slice type | see slice type |
20-
// | functions | Function | func(...interface{}) *js.Object |
21-
// | time.Time | Date | time.Time |
22-
// | - | instanceof Node | *js.Object |
23-
// | maps, structs | instanceof Object | map[string]interface{} |
7+
// Internalization
248
//
25-
// Additionally, for a struct containing a *js.Object field, only the content of the field will be passed to JavaScript and vice versa.
9+
// When values pass from Javascript to Go, a process known as internalization,
10+
// the following conversion table is applied:
11+
//
12+
// |----------------+-------------------------+---------------+--------|
13+
// | Go target type | Javascript source value | Translation | Result |
14+
// |----------------+-------------------------+---------------+--------|
15+
// | string | null | UTF16 -> UTF8 | "" |
16+
// | | undefined | | "" |
17+
// | | "" | | "" |
18+
// | | new String("") | | "" |
19+
// | | "ok" † | | "ok" |
20+
// | | new String("ok") † | | "ok" |
21+
// |----------------+-------------------------+---------------+--------|
22+
// | bool | null | none | false |
23+
// | | undefined | | false |
24+
// | | false † | | false |
25+
// | | new Boolean(false) † | | false |
26+
// |----------------+-------------------------+---------------+--------|
27+
//
28+
// Any source values not listed in this table cause a runtime panic for a given
29+
// target type if a conversion is attempted, e.g. a Javascript number value
30+
// being assigned to a string type Go variable.
31+
//
32+
// Source values annotated with † are generally applicable to all valid
33+
// values of the target type. e.g. for target type string, "ok" represents
34+
// all valid string values.
35+
//
36+
// Externalization
37+
//
38+
// When values pass from Go to Javascript, a process known as externalization,
39+
// the following conversion table is applied:
40+
//
41+
// To follow
42+
//
43+
// Additionally, for a struct containing a *js.Object field, only the content
44+
// of the field will be passed to JavaScript and vice versa.
2645
package js
2746

2847
// Object is a container for a native JavaScript object. Calls to its methods are treated specially by GopherJS and translated directly to their JavaScript syntax. A nil pointer to Object is equal to JavaScript's "null". Object can not be used as a map key.

js/js_test.go

Lines changed: 207 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ func TestCallWithNull(t *testing.T) {
467467
func TestReflection(t *testing.T) {
468468
o := js.Global.Call("eval", "({ answer: 42 })")
469469
if reflect.ValueOf(o).Interface().(*js.Object) != o {
470-
t.Fail()
470+
t.Fatal()
471471
}
472472

473473
type S struct {
@@ -477,18 +477,19 @@ func TestReflection(t *testing.T) {
477477

478478
v := reflect.ValueOf(&s).Elem()
479479
if v.Field(0).Interface().(*js.Object).Get("answer").Int() != 42 {
480-
t.Fail()
480+
t.Fatal()
481481
}
482482
if v.Field(0).MethodByName("Get").Call([]reflect.Value{reflect.ValueOf("answer")})[0].Interface().(*js.Object).Int() != 42 {
483-
t.Fail()
483+
t.Fatal()
484484
}
485485
v.Field(0).Set(reflect.ValueOf(js.Global.Call("eval", "({ answer: 100 })")))
486486
if s.Field.Get("answer").Int() != 100 {
487-
t.Fail()
487+
t.Fatal()
488488
}
489489

490-
if fmt.Sprintf("%+v", s) != "{Field:[object Object]}" {
491-
t.Fail()
490+
expFmt := "{Field:[object Object]}"
491+
if v := fmt.Sprintf("%+v", s); v != expFmt {
492+
t.Fatalf("fmt out was: %q; expected %q", v, expFmt)
492493
}
493494
}
494495

@@ -571,3 +572,203 @@ func TestUint8Array(t *testing.T) {
571572
t.Errorf("Non-empty byte array is not externalized as a Uint8Array")
572573
}
573574
}
575+
576+
type Internalize struct {
577+
*js.Object
578+
579+
string string `js:"string"`
580+
bool bool `js:"bool"`
581+
}
582+
583+
func TestInternalizeString(t *testing.T) {
584+
fieldName := "string"
585+
zero := ""
586+
587+
s := &Internalize{Object: js.Global.Get("Object").New()}
588+
589+
// *************
590+
// undefined
591+
// *************
592+
s.Object.Set(fieldName, jsundefined())
593+
594+
// via struct field
595+
if v := s.string; v != zero {
596+
t.Fatalf("expected string field to be %q, got %q", zero, v)
597+
}
598+
599+
// via *js.Object.String()
600+
if v := s.Object.Get(fieldName).String(); v != zero {
601+
t.Fatalf("expected string field via *js.Object.String() to be %q, got %q", zero, v)
602+
}
603+
604+
// *************
605+
// null
606+
// *************
607+
s.Object.Set(fieldName, jsnull())
608+
609+
// via struct field
610+
if v := s.string; v != zero {
611+
t.Fatalf("expected string field to be %q, got %q", zero, v)
612+
}
613+
614+
// via *js.Object.String()
615+
if v := s.Object.Get(fieldName).String(); v != zero {
616+
t.Fatalf("expected string field via *js.Object.String() to be %q, got %q", zero, v)
617+
}
618+
619+
// *************
620+
// valid primitive string
621+
// *************
622+
exp := "ok"
623+
s.Object.Set(fieldName, jsval(`"`+exp+`"`))
624+
625+
// via struct field
626+
if v := s.string; v != exp {
627+
t.Fatalf("expected string field to be %q, got %q", zero, v)
628+
}
629+
630+
// via *js.Object.String()
631+
if v := s.Object.Get(fieldName).String(); v != exp {
632+
t.Fatalf("expected string field via *js.Object.String() to be %q, got %q", zero, v)
633+
}
634+
635+
// *************
636+
// valid String object
637+
// *************
638+
s.Object.Set(fieldName, jsval(`new String("`+exp+`")`))
639+
640+
// via struct field
641+
if v := s.string; v != exp {
642+
t.Fatalf("expected string field to be %q, got %q", zero, v)
643+
}
644+
645+
// via *js.Object.String()
646+
if v := s.Object.Get(fieldName).String(); v != exp {
647+
t.Fatalf("expected string field via *js.Object.String() to be %q, got %q", zero, v)
648+
}
649+
650+
// *************
651+
// invalid
652+
// *************
653+
s.Object.Set(fieldName, jsval("5"))
654+
655+
shouldRuntimePanic(t, "runtime error: tried to internalize non-string value of type number", func() {
656+
_ = s.string
657+
})
658+
659+
shouldRuntimePanic(t, "runtime error: tried to internalize non-string value of type number", func() {
660+
_ = s.Object.Get(fieldName).String()
661+
})
662+
}
663+
664+
func TestInternalizeBool(t *testing.T) {
665+
fieldName := "bool"
666+
zero := false
667+
668+
s := &Internalize{Object: js.Global.Get("Object").New()}
669+
670+
// *************
671+
// undefined
672+
// *************
673+
s.Object.Set(fieldName, jsundefined())
674+
675+
// via struct field
676+
if v := s.bool; v != zero {
677+
t.Fatalf("expected bool field to be %q, got %q", zero, v)
678+
}
679+
680+
// via *js.Object.Bool()
681+
if v := s.Object.Get(fieldName).Bool(); v != zero {
682+
t.Fatalf("expected bool field via *js.Object.Bool() to be %q, got %q", zero, v)
683+
}
684+
685+
// *************
686+
// null
687+
// *************
688+
s.Object.Set(fieldName, jsnull())
689+
690+
// via struct field
691+
if v := s.bool; v != zero {
692+
t.Fatalf("expected bool field to be %q, got %q", zero, v)
693+
}
694+
695+
// via *js.Object.Bool()
696+
if v := s.Object.Get(fieldName).Bool(); v != zero {
697+
t.Fatalf("expected bool field via *js.Object.Bool() to be %q, got %q", zero, v)
698+
}
699+
700+
// *************
701+
// valid primitive bool
702+
// *************
703+
exp := true
704+
s.Object.Set(fieldName, jsval(`true`))
705+
706+
// via struct field
707+
if v := s.bool; v != exp {
708+
t.Fatalf("expected bool field to be %v, got %v", exp, v)
709+
}
710+
711+
// via *js.Object.Bool()
712+
if v := s.Object.Get(fieldName).Bool(); v != exp {
713+
t.Fatalf("expected bool field via *js.Object.Bool() to be %q, got %q", zero, v)
714+
}
715+
716+
// *************
717+
// valid Bool object
718+
// *************
719+
s.Object.Set(fieldName, jsval(`new Boolean(true)`))
720+
721+
// via struct field
722+
if v := s.bool; v != exp {
723+
t.Fatalf("expected bool field to be %q, got %q", zero, v)
724+
}
725+
726+
// via *js.Object.Bool()
727+
if v := s.Object.Get(fieldName).Bool(); v != exp {
728+
t.Fatalf("expected bool field via *js.Object.Bool() to be %q, got %q", zero, v)
729+
}
730+
731+
// *************
732+
// invalid
733+
// *************
734+
s.Object.Set(fieldName, jsval("5"))
735+
736+
shouldRuntimePanic(t, "runtime error: tried to internalize non-bool value of type number", func() {
737+
_ = s.bool
738+
})
739+
740+
shouldRuntimePanic(t, "runtime error: tried to internalize non-bool value of type number", func() {
741+
_ = s.Object.Get(fieldName).Bool()
742+
})
743+
}
744+
745+
func jsval(v string) *js.Object {
746+
return js.Global.Call("eval", v)
747+
}
748+
749+
func jsnull() *js.Object {
750+
return js.Global.Call("eval", "null")
751+
}
752+
753+
func jsundefined() *js.Object {
754+
return js.Global.Call("eval", "undefined")
755+
}
756+
757+
func consolelog(args ...interface{}) {
758+
js.Global.Get("console").Call("log", args...)
759+
}
760+
761+
func shouldRuntimePanic(t *testing.T, msg string, f func()) {
762+
defer func() {
763+
err, ok := recover().(error)
764+
if !ok {
765+
t.Fatalf("expected to have had to handle panic; we didn't see a panic")
766+
}
767+
768+
if err.Error() != msg {
769+
t.Fatalf("expected error %q, got %q", msg, err)
770+
}
771+
}()
772+
773+
f()
774+
}

0 commit comments

Comments
 (0)