-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathAliensForJS.ns
More file actions
272 lines (251 loc) · 10.9 KB
/
Copy pathAliensForJS.ns
File metadata and controls
272 lines (251 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
Newspeak3
'Newspeak'
class AliensForJS usingPlatform: p = Object new (
(* Aliens are a capability-based API for a foreign function interface (FFI). On NS2JS, they allow Newspeak code to invoke JavaScript code and vice versa.
An Alien is a Newspeak proxy for a JavaScript object. Upon receiving a message, an Alien expatriates the arguments, sends the message to the target JavaScript object, and alienates the result.
An Expat is a JavaScript proxy for a Newspeak object. Upon receiving a message, an Expat alienates the arguments, sends the message to the target Newspeak object, and expatriates the result.
A bilingual object is one whose representation is the same in both languages: unwrapped basic types such as numbers, booleans. Note that even though Newspeak and JavaScript closures have the same representation, they are not bilingual and wrapping should occur because Newspeak closures expect Newspeak/Alien arguments and JavaScript closures expect JavaScript/Expat arguments. What about strings and arrays? It would seem we have to wrap arrays because their elements should be alieniated/expatriated. It may be safe to treat strings as bilingual if they always respond to messages with other bilingual objects.
Alien mappings:
alien sort: a ignored: b ignored: c -> alien.sort(a, b, c)
alien new: a ignored: b ignored: c -> new alien(a, b, c)
alien at: 'a' -> alien.a
alien at: 'a' put: b -> alien.a = b
Expat mappings:
? *)
|
public global = Alien wrapping: (_js ident: 'theGlobalObject').
public undefined = Alien wrapping: (_js ident: 'undefined').
|
) (
class Alien wrapping: o = Object new (
_js assign: (_js propertyOf: self at: (_js literal: 'jsTarget')) toBe: o.
) (
public at: key = (
^alienate: (_js call: (_js propertyOf: (_js ident: 'Reflect')
at: (_js literal: 'get'))
with: {_js propertyOf: self at: (_js literal: 'jsTarget'). expatriate: key. })
)
public at: key put: value = (
_js call: (_js propertyOf: (_js ident: 'Reflect') at: (_js literal: 'set'))
with: {_js propertyOf: self at: (_js literal: 'jsTarget'). expatriate: key. expatriate: value. }.
^value
)
doesNotUnderstand: msg = (
| jsTarget jsArguments jsSelector jsResult |
jsTarget:: _js propertyOf: self at: (_js literal: 'jsTarget').
jsArguments:: msg arguments collect: [:arg | expatriate: arg ].
jsSelector:: copyUntilFirstColon: msg mangledSelector.
jsResult:: jsSelector = 'new' ifTrue: [
_js call: (_js propertyOf: (_js ident: 'Reflect')
at: (_js literal: 'construct'))
with: {jsTarget. jsArguments. }
]
ifFalse: [
| jsFunction |
jsFunction:: _js propertyOf: jsTarget at: jsSelector.
(_js operator: '===' with: jsFunction and: (_js ident: 'undefined')) ifTrue: [
^super doesNotUnderstand: msg
].
_js call: (_js propertyOf: (_js ident: 'Reflect')
at: (_js literal: 'apply'))
with: {jsFunction. jsTarget. jsArguments. }
].
^alienate: jsResult
)
public isUndefined = (
^_js operator: '==='
with: (_js propertyOf: self at: (_js literal: 'jsTarget'))
and: (_js ident: 'undefined')
)
public printString = (
self isUndefined ifTrue: [ ^'undefined' ].
(* undefined.toString() throws in JS *)
^_js call: (_js propertyOf: (_js propertyOf: self
at: (_js literal: 'jsTarget'))
at: (_js literal: 'toString'))
with: {}
)
public value = (
| jsTarget = _js propertyOf: self at: (_js literal: 'jsTarget'). |
^alienate: (_js call: (_js propertyOf: jsTarget at: (_js literal: 'call'))
with: {jsTarget. })
)
public value: a1 = (
| jsTarget = _js propertyOf: self at: (_js literal: 'jsTarget'). |
^alienate: (_js call: (_js propertyOf: jsTarget at: (_js literal: 'call'))
with: {jsTarget. expatriate: a1. })
)
public value: a1 value: a2 = (
| jsTarget = _js propertyOf: self at: (_js literal: 'jsTarget'). |
^alienate: (_js call: (_js propertyOf: jsTarget at: (_js literal: 'call'))
with: {jsTarget. expatriate: a1. expatriate: a2. })
)
public value: a1 value: a2 value: a3 = (
| jsTarget = _js propertyOf: self at: (_js literal: 'jsTarget'). |
^alienate: (_js call: (_js propertyOf: jsTarget at: (_js literal: 'call'))
with: {jsTarget. expatriate: a1. expatriate: a2. expatriate: a3. })
)
public valueWithArguments: args = (
| jsTarget = _js propertyOf: self at: (_js literal: 'jsTarget'). |
^alienate: (_js call: (_js propertyOf: jsTarget at: (_js literal: 'apply'))
with: {jsTarget. args collect: [:nsArg | expatriate: nsArg ]. })
)
public isKindOfJSAlien ^<Boolean> = (
^true
)
public includesKey: key = (
(* JS [key in jsTarget] *)
^_js operator: 'in'
with: (expatriate: key)
and: (_js propertyOf: self at: (_js literal: 'jsTarget'))
)
public instanceOf: ctor = (
(* JS [jsTarget instanceof ctor] *)
^_js operator: 'instanceof'
with: (_js propertyOf: self at: (_js literal: 'jsTarget'))
and: (expatriate: ctor)
)
public removeKey: key = (
(* JS [Reflect.deleteProperty(jsTarget, key)] *)
^_js call: (_js propertyOf: (_js ident: 'Reflect')
at: (_js literal: 'deleteProperty'))
with: {_js propertyOf: self at: (_js literal: 'jsTarget'). expatriate: key. }
)
public newWithArguments: nsArguments = (
(* JS [new jsTarget(...nsArguments)] via Reflect.construct. *)
| jsTarget jsArguments |
jsTarget:: _js propertyOf: self at: (_js literal: 'jsTarget').
jsArguments:: nsArguments collect: [:arg | expatriate: arg ].
^alienate: (_js call: (_js propertyOf: (_js ident: 'Reflect')
at: (_js literal: 'construct'))
with: {jsTarget. jsArguments. })
)
public perform: jsSelector withArguments: nsArguments = (
(* Reflectively invoke the JS method [jsSelector] on the wrapped target
with [nsArguments]. Arguments are expatriated; result is alienated.
For explicit dispatch from client code; the implicit-DNU dispatch path
in [doesNotUnderstand:] still uses its inline logic to avoid the
double-expatriate. *)
| jsTarget jsFunction jsArguments |
jsTarget:: _js propertyOf: self at: (_js literal: 'jsTarget').
jsFunction:: _js propertyOf: jsTarget at: jsSelector.
(_js operator: '===' with: jsFunction and: (_js ident: 'undefined')) ifTrue: [
^Error signal: 'JS target has no method named: ' , jsSelector
].
jsArguments:: nsArguments collect: [:arg | expatriate: arg ].
^alienate: (_js call: (_js propertyOf: (_js ident: 'Reflect')
at: (_js literal: 'apply'))
with: {jsFunction. jsTarget. jsArguments. })
)
public isKindOfClosure ^<Boolean> = (
(* An Alien wrapping a JS function is not a Newspeak Closure. *)
^false
)
public isKindOfImage ^<Boolean> = (
^self instanceOf: (global at: #HTMLImageElement)
)
) : ()
class Expat wrapping: o = Object new (
(* :todo: Implement with ES6 Proxy instead. *)
_js assign: (_js propertyOf: self at: (_js literal: 'nsTarget')) toBe: o.
) (
public isKindOfJSAlien ^<Boolean> = (
^false
)
public isKindOfExpat ^<Boolean> = (
(* [alienate:] / [expatriate:] both call [isKindOfExpat] on candidates
inside the [Object.runtimeClass.basicNew] branch. Without this
override on Expat (and the default false on Object in KernelForJS),
the call DNU'd silently. *)
^true
)
) : ()
expatriateBlock: b = (
^_js functionOf: {}
body: (_js return: (expatriate: (b valueWithArguments: ((_js verbatim: 'Array.prototype.slice.call(arguments, 0)') collect: [:ea |
alienate: ea
]))))
)
alienate: jsObj = (
(* Wrap a value coming back from JS so Newspeak code can handle it.
Bilingual primitives (number/string/boolean) pass through; functions
become Aliens; raw Newspeak objects (already instances of an NS
runtime class) are either rejected or, if they are Expats, unwrapped
to their nsTarget.
Typeof comparisons use [==] (not [=]) because the compiler inlines
[==] to JS [===] with the correct operator precedence:
(typeof X) == 'string' → (typeof X) === 'string' ✓
Using [=] compiles to a String>>= method call without parens around
the receiver, and JS [typeof X.$$equal('string')] parses as
typeof (X.$$equal('string')) ✗ (typeof binds to the call result) *)
(_js operator: '===' with: (_js ident: 'null') and: jsObj) ifTrue: [ ^nil ].
(_js operator: '===' with: (_js ident: 'undefined') and: jsObj) ifTrue: [
^undefined
].
(_js prefixOperator: 'typeof ' on: jsObj) == 'string' ifTrue: [ ^jsObj ].
(_js prefixOperator: 'typeof ' on: jsObj) == 'number' ifTrue: [ ^jsObj ].
(_js prefixOperator: 'typeof ' on: jsObj) == 'boolean' ifTrue: [ ^jsObj ].
(* This does not discrimate NS vs JS closures *)
(_js prefixOperator: 'typeof ' on: jsObj) == 'function' ifTrue: [
^Alien wrapping: jsObj
].
(_js operator: 'instanceof'
with: jsObj
and: (_js propertyOf: (_js propertyOf: Object
at: (_js literal: 'runtimeClass'))
at: (_js literal: 'basicNew'))) ifTrue: [
jsObj isKindOfJSAlien ifTrue: [
Error signal: 'Shouldnt be asked to double alienate...'
].
jsObj isKindOfExpat ifTrue: [
^_js propertyOf: jsObj at: (_js literal: 'nsTarget')
].
Error signal: 'Asked to alienate a raw Newspeak object...'
].
(_js operator: 'instanceof' with: jsObj and: (_js ident: 'Uint8Array')) ifTrue: [
^jsObj
].
^Alien wrapping: jsObj
)
expatriate: nsObj = (
(* Inverse of [alienate:]. Bilingual primitives pass through; Newspeak
closures become JS functions; Aliens are unwrapped to their jsTarget;
raw NS objects get wrapped in an Expat. See [alienate:] for why these
typeof checks use [==] rather than [=]. *)
(_js operator: '===' with: nil and: nsObj) ifTrue: [ ^_js ident: 'null' ].
(_js prefixOperator: 'typeof ' on: nsObj) == 'string' ifTrue: [ ^nsObj ].
(_js prefixOperator: 'typeof ' on: nsObj) == 'number' ifTrue: [ ^nsObj ].
(_js prefixOperator: 'typeof ' on: nsObj) == 'boolean' ifTrue: [ ^nsObj ].
(* This does not discrimate NS vs JS closures *)
(_js prefixOperator: 'typeof ' on: nsObj) == 'function' ifTrue: [
^expatriateBlock: nsObj
].
(_js operator: 'instanceof'
with: nsObj
and: (_js propertyOf: (_js propertyOf: Object
at: (_js literal: 'runtimeClass'))
at: (_js literal: 'basicNew'))) ifTrue: [
nsObj isKindOfJSAlien ifTrue: [
^_js propertyOf: nsObj at: (_js literal: 'jsTarget')
].
nsObj isKindOfExpat ifTrue: [
Error signal: 'Shouldnt be asked to double expatriate...'
].
^Expat wrapping: nsObj
].
(_js operator: 'instanceof' with: nsObj and: (_js ident: 'Uint8Array')) ifTrue: [
^nsObj
].
Error signal: 'Asked to expatriate a raw JS object...'
)
public localStorage ^<Alien> = (
^(global at: #window) at: #localStorage
)
copyUntilFirstColon: sel = (
#BOGUS.
(* DNU does not yet pass unmangled selectors. *)
2 to: sel size
do: [:i | (sel at: i) = ("$" at: 1) ifTrue: [ ^sel copyFrom: 2 to: i - 1 ] ].
^sel copyFrom: 2 to: sel size
)
) : ()