May see this differently by the end of the week, but for the moment, I seem to be finding workable middle ground in:
- Avoiding the use of a property or global reference by wrapping the JSContext reference in
script ... end script
- Returning all data from the JSContext as a predictable (NSDictionary) type, with separate
type
and value
keys.
Avoiding the property / global reference should make it a bit less accident-prone on the ‘Can’t save script containing pointers’ front, and a script reference still gives us reuse of a single JSContext() ( subject to the caveats about persistent JS name-bindings within a single AS session ).
NSDictionary -> record lets us know what to expect at the AppleScript end, and allows for a bit more clarity or specificity about JS return types like NaN
and undefined
;
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "JavaScriptCore"
use scripting additions
on run
set ca to current application
script math
property JSC : ca's JSContext's new()
-- eval :: String -> {jsType::String, value::a}
on eval(strJS)
set mb to JSC's evaluateScript:("(() => { const v = (() => " & ¬
"{ return " & strJS & "})(), t = typeof v; " & ¬
" return {jsType: t !== 'object' ? (t !== 'undefined' ? t : '⊥') : " & ¬
"(Array.isArray(v) ? 'array' : 'object')," & ¬
" value: t !== 'function' ? ( t !== 'undefined' ? v : null) : " & ¬
"v.toString()}})()")
if mb's isUndefined() then
{jsType:"⊥", value:missing value}
else
mb's toDictionary() as record
end if
end eval
-- mathFn :: String -> Number
on mathFn(k, n)
set rec to eval(("Math." & k & "(" & n as string) & ")")
if jsType of rec is "number" then
value of rec
else
missing value
end if
end mathFn
-- MATH FUNCTIONS ----------------------------------------------------
-- sin :: Real -> Real
on sin(x)
mathFn("sin", x)
end sin
-- cos :: Real -> Real
on cos(x)
mathFn("cos", x)
end cos
-- tan :: Real -> Real
on tan(x)
-- but xs == Pi/2 may deserve special handling
mathFn("tan", x)
end tan
-- sqrt :: Real -> Real
on sqrt(x)
-- NaN where 0 > x
mathFn("sqrt", x)
end sqrt
-- logBase :: Real -> Real -> Real
on logBase(x, y)
set rec to eval((("Math.log(" & y as string) & ¬
")/Math.log(" & x as string) & ")")
if jsType of rec is "number" then
value of rec
else
missing value
end if
end logBase
end script
-- TEST ---------------------------------------------------------
tell math
{logBase(2, 8), sin(0.25), cos("frog"), tan(pi / 4), ¬
tan(pi / 2), sqrt(-8), sqrt(8), ¬
eval("Object.getOwnPropertyNames(this)")}
end tell
end run
{3.0, 0.247403959255, missing value, 0.999999999999, -9.67009938079218E+12, NaN, 2.828427124746, {value:{"Infinity", "NaN", "undefined", "parseFloat", "isNaN", "isFinite", "escape", "unescape", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "EvalError", "ReferenceError", "SyntaxError", "URIError", "Proxy", "JSON", "Math", "Atomics", "console", "Int8Array", "Int16Array", "Int32Array", "Uint8Array", "Uint8ClampedArray", "Uint16Array", "Uint32Array", "Float32Array", "Float64Array", "DataView", "Set", "Date", "Boolean", "Number", "WeakMap", "WeakSet", "parseInt", "Object", "Function", "Array", "RegExp", "RangeError", "TypeError", "ArrayBuffer", "SharedArrayBuffer", "String", "Symbol", "Error", "Map", "Promise", "eval", "Intl", "Reflect"}, jsType:"array"}}