subscript 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -34
- package/{src/evaluate.js → evaluate.js} +2 -2
- package/justin.js +64 -38
- package/justin.min.js +1 -1
- package/package.json +3 -2
- package/parse.js +134 -0
- package/subscript.js +2 -2
- package/subscript.min.js +1 -1
- package/src/parse.js +0 -132
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# <!--<img alt="subscript" src="/subscript2.svg" height=42/>--> sub͘<em>script</em> <!--<sub>SUB͘<em>SCRIPT</em></sub>-->
|
|
2
2
|
|
|
3
|
-
_Subscript_ is micro-language with common syntax subset of C++, JS, Java, Python, Go, Rust.<br/>
|
|
3
|
+
_Subscript_ is micro-language with common syntax subset of C++, JS, Java, Python, Go, Rust, Swift, Objective C, Kotlin etc.<br/>
|
|
4
4
|
|
|
5
5
|
* Well-known syntax
|
|
6
6
|
* Any _subscript_ fragment can be copy-pasted to any target language
|
|
7
7
|
* It's tiny <sub></sub>
|
|
8
|
-
* It's
|
|
8
|
+
* It's :rocket: fast ([see performance](#performance))
|
|
9
9
|
* Configurable & extensible
|
|
10
10
|
* Trivial to use...
|
|
11
11
|
|
|
@@ -15,7 +15,9 @@ let fn = subscript(`a.b + c(d - 1)`)
|
|
|
15
15
|
fn({ a: { b:1 }, c: x => x * 2, d: 3 }) // 5
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## Motivation
|
|
19
|
+
|
|
20
|
+
_Subscript_ is designed to be useful for:
|
|
19
21
|
|
|
20
22
|
* templates (perfect match with [template parts](https://github.com/github/template-parts))
|
|
21
23
|
* expressions evaluators, calculators
|
|
@@ -24,12 +26,9 @@ fn({ a: { b:1 }, c: x => x * 2, d: 3 }) // 5
|
|
|
24
26
|
* sandboxes, playgrounds, safe eval
|
|
25
27
|
* custom DSL
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<time datetime="{{ date.toUTCString() }}">{{ date.toLocaleTimeString() }}</time>
|
|
31
|
-
</template>
|
|
32
|
-
```
|
|
29
|
+
[_Jsep_](https://github.com/EricSmekens/jsep) is generally fine for the listed tasks, unless you need dependencies as small as possible.
|
|
30
|
+
_Subscript_ has [2.5kb](https://npmfs.com/package/subscript/3.0.0/subscript.min.js) footprint vs [11.4kb](https://npmfs.com/package/jsep/1.2.0/dist/jsep.min.js) _jsep_, with better performance.
|
|
31
|
+
|
|
33
32
|
|
|
34
33
|
## Operators
|
|
35
34
|
|
|
@@ -48,17 +47,53 @@ Default operators include common operators for the listed languages in the follo
|
|
|
48
47
|
* `&&`
|
|
49
48
|
* `||`
|
|
50
49
|
|
|
51
|
-
All other operators can be extended
|
|
50
|
+
All other operators can be extended, see [extension](#extension).
|
|
51
|
+
|
|
52
|
+
## Evaluation
|
|
53
|
+
|
|
54
|
+
_Subscript_ parser generates lispy calltree (compatible with [frisk](https://npmjs.com/frisk)), which is compared to esprima AST has:
|
|
55
|
+
|
|
56
|
+
+ minimal possible overhead
|
|
57
|
+
+ clear precedence
|
|
58
|
+
+ overloading by context
|
|
59
|
+
+ manual evaluation and debugging
|
|
60
|
+
+ conventional form
|
|
61
|
+
+ one-liner docs:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import {evaluate} from 'subscript.js'
|
|
65
|
+
|
|
66
|
+
evaluate(['+', ['*', 'min', 60], '"sec"'], { min: 5 }) // min*60 + "sec" == "300sec"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Extension
|
|
70
|
+
|
|
71
|
+
Tokens are extensible via `parse.token` list, can be added support of _regex_, _array_, _object_, _interpolated string_ and others.
|
|
72
|
+
Default tokens include:
|
|
73
|
+
|
|
74
|
+
* `"abc"` strings
|
|
75
|
+
* `1.2e+3` floats
|
|
76
|
+
* `()` expression groups or fn calls
|
|
77
|
+
* `.`, `[]` property access
|
|
78
|
+
|
|
79
|
+
Operators can be extended via `parse.operator` to add support for any unary/binary/postfix operators, calls, props or chains.
|
|
52
80
|
|
|
81
|
+
Comments can be added via extending `parse.space`.
|
|
82
|
+
|
|
83
|
+
For now see justin extension how things can be done.
|
|
84
|
+
|
|
85
|
+
<!--
|
|
53
86
|
```js
|
|
54
87
|
import { parse, evaluate } from 'subscript.js'
|
|
55
88
|
|
|
56
89
|
// add precedences
|
|
57
|
-
|
|
90
|
+
// TODO
|
|
91
|
+
// parse.operator['=>'] = 10
|
|
58
92
|
|
|
59
93
|
// define evaluators
|
|
60
|
-
|
|
61
|
-
evaluate.operator['
|
|
94
|
+
// TODO
|
|
95
|
+
// evaluate.operator['=>'] = ( args, body ) => evaluate(body, args)
|
|
96
|
+
// evaluate.operator['|'] = ( a, ...b ) => a.pipe(...b)
|
|
62
97
|
|
|
63
98
|
let tree = parse(`
|
|
64
99
|
interval(350)
|
|
@@ -68,28 +103,13 @@ let tree = parse(`
|
|
|
68
103
|
`)
|
|
69
104
|
evaluate(tree, { Math, map, take, interval, gaussian })
|
|
70
105
|
```
|
|
71
|
-
|
|
72
|
-
## Extending
|
|
73
|
-
|
|
74
|
-
By default subscript detects the following tokens:
|
|
75
|
-
|
|
76
|
-
* `"` strings
|
|
77
|
-
* `1.2e+3` floats
|
|
78
|
-
* `true`, `false`, `null` literals
|
|
79
|
-
* `()` expression groups or fn calls
|
|
80
|
-
* `.`, `[]` property access
|
|
81
|
-
|
|
82
|
-
Literals can be extended via `parse.literal` dict.
|
|
83
|
-
|
|
84
|
-
Token parsers are extensible via `parse.token` dict, can be added support of _regex_, _array_, _object_, _interpolated string_ and others.
|
|
85
|
-
|
|
86
|
-
Postfix parsers are applied to parsed tokens and can be used to provide _property chains_, _function calls_, _postfix operators_, _token mapping_, _ternary operators_ and so on. They're extensible via `parse.postfix`.
|
|
106
|
+
-->
|
|
87
107
|
|
|
88
108
|
|
|
89
109
|
## Justin
|
|
90
110
|
|
|
91
111
|
_Justin_ extension (original [thread](https://github.com/endojs/Jessie/issues/66)) is minimal JS subset − JSON with JS expressions.<br/>
|
|
92
|
-
It adds support
|
|
112
|
+
It adds support of:
|
|
93
113
|
|
|
94
114
|
+ `**` binary operator
|
|
95
115
|
+ `~` unary operator
|
|
@@ -99,8 +119,10 @@ It adds support for:
|
|
|
99
119
|
+ `{...}` Object literal
|
|
100
120
|
+ `in` binary operator
|
|
101
121
|
+ `;` expression separator
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
+ unary word operators
|
|
123
|
+
+ `//`, `/* */` comments
|
|
124
|
+
+ `true`, `false`, `null`, `undefined` literals
|
|
125
|
+
<!-- + `===`, `!==` operators -->
|
|
104
126
|
<!-- + `?` chaining operator -->
|
|
105
127
|
<!-- + `...x` unary operator -->
|
|
106
128
|
<!-- + strings interpolation -->
|
|
@@ -117,6 +139,14 @@ evaluate(xy) // 1
|
|
|
117
139
|
|
|
118
140
|
These are custom DSL operators snippets for your inspiration:
|
|
119
141
|
|
|
142
|
+
|
|
143
|
+
```html
|
|
144
|
+
template-parts proposal
|
|
145
|
+
<template id="timer">
|
|
146
|
+
<time datetime="{{ date.toUTCString() }}">{{ date.toLocaleTimeString() }}</time>
|
|
147
|
+
</template>
|
|
148
|
+
```
|
|
149
|
+
|
|
120
150
|
// a.b.c
|
|
121
151
|
// (node, c) => c === PERIOD ? (index++, space(), ['.', node, '"'+id()+'"']) : node,
|
|
122
152
|
|
|
@@ -249,7 +279,7 @@ Subscript shows relatively good performance within other evaluators:
|
|
|
249
279
|
// 1 + (a * b / c % d) - 2.0 + -3e-3 * +4.4e4 / f.g[0] - i.j(+k == 1)(0)
|
|
250
280
|
// parse 30k times
|
|
251
281
|
|
|
252
|
-
subscript: ~
|
|
282
|
+
subscript: ~220 ms
|
|
253
283
|
jsep: ~280 ms
|
|
254
284
|
expr-eval: ~480 ms
|
|
255
285
|
jexl: ~1200 ms
|
|
@@ -265,6 +295,6 @@ new Function: ~1400 ms
|
|
|
265
295
|
* [expression-eval](https://github.com/donmccurdy/expression-eval)
|
|
266
296
|
* [jsep](https://github.com/EricSmekens/jsep)
|
|
267
297
|
* [string-math](https://github.com/devrafalko/string-math)
|
|
268
|
-
|
|
298
|
+
* [nerdamer](https://github.com/jiggzson/nerdamer)
|
|
269
299
|
|
|
270
300
|
<p align=center>🕉</p>
|
package/justin.js
CHANGED
|
@@ -1,73 +1,99 @@
|
|
|
1
1
|
// justin lang https://github.com/endojs/Jessie/issues/66
|
|
2
|
-
import {evaluate
|
|
3
|
-
import {parse,
|
|
4
|
-
|
|
2
|
+
import {evaluate} from './evaluate.js'
|
|
3
|
+
import {parse, code, char, skip, expr, nil, binary, unary} from './parse.js'
|
|
4
|
+
|
|
5
|
+
// ;
|
|
6
|
+
parse.operator[0] = binary(c => c==44||c==59)
|
|
5
7
|
|
|
6
8
|
// undefined
|
|
7
|
-
|
|
9
|
+
parse.token.splice(3,0, c =>
|
|
10
|
+
c === 116 && char(4) === 'true' && skip(4) ? true :
|
|
11
|
+
c === 102 && char(5) === 'false' && skip(5) ? false :
|
|
12
|
+
c === 110 && char(4) === 'null' && skip(4) ? null :
|
|
13
|
+
c === 117 && char(9) === 'undefined' && skip(9) ? undefined :
|
|
14
|
+
undefined
|
|
15
|
+
)
|
|
8
16
|
|
|
9
|
-
// '
|
|
10
|
-
token
|
|
17
|
+
// "' with /
|
|
18
|
+
parse.token[1] = (q, qc, c, str) => {
|
|
19
|
+
if (q !== 34 && q !== 39) return
|
|
20
|
+
qc = char(), skip(), str = ''
|
|
21
|
+
while (c=code(), c-q) {
|
|
22
|
+
if (c === 92) skip(), str += escape[char()] || char(); else str+=char()
|
|
23
|
+
skip()
|
|
24
|
+
}
|
|
25
|
+
return skip(), qc + str + qc
|
|
26
|
+
}
|
|
27
|
+
const escape = {n:'\n', r:'\r', t:'\t', b:'\b', f:'\f', v:'\v'}
|
|
11
28
|
|
|
12
29
|
// **
|
|
13
|
-
|
|
14
|
-
operator
|
|
30
|
+
evaluate.operator['**'] = (...args)=>args.reduceRight((a,b)=>Math.pow(b,a))
|
|
31
|
+
parse.operator.splice(parse.operator.length - 3, 0, binary(cc=>cc===42 && code(1) === cc && 2))
|
|
15
32
|
|
|
16
33
|
// ~
|
|
17
|
-
unary
|
|
18
|
-
operator['~'] = a=>~a
|
|
34
|
+
parse.operator.splice(parse.operator.length-2, 0, unary(cc => cc === 126))
|
|
35
|
+
evaluate.operator['~'] = a=>~a
|
|
19
36
|
|
|
20
|
-
// ...
|
|
21
|
-
// unary[1]['...']=true
|
|
37
|
+
// TODO ...
|
|
38
|
+
// // unary[1]['...']=true
|
|
22
39
|
|
|
23
|
-
// ;
|
|
24
|
-
binary[';'] = 1
|
|
25
40
|
|
|
26
41
|
// ?:
|
|
27
|
-
operator['?:']=(a,b,c)=>a?b:c
|
|
28
|
-
|
|
42
|
+
evaluate.operator['?:']=(a,b,c)=>a?b:c
|
|
43
|
+
parse.operator.splice(1,0, (node,cc,prec,end) => {
|
|
29
44
|
let a, b
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
if (cc !== 63) return
|
|
46
|
+
if (node===nil) err('Bad expression')
|
|
47
|
+
skip(), parse.space(), a = expr(-1,58)
|
|
48
|
+
if (code() !== 58) return
|
|
49
|
+
skip(), parse.space(), b = expr()
|
|
50
|
+
return ['?:', node, a, b]
|
|
35
51
|
})
|
|
36
52
|
|
|
37
53
|
// /**/, //
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
parse.space = cc => {
|
|
55
|
+
while (cc = code(), cc <= 32) {
|
|
56
|
+
skip()
|
|
57
|
+
if (code() === 47)
|
|
58
|
+
// /**/
|
|
59
|
+
if (code(1) === 42) skip(2), skip(c => c !== 42 && code(1) !== 47), skip(2)
|
|
60
|
+
// //
|
|
61
|
+
else if (code(1) === 47) skip(2), skip(c => c >= 32)
|
|
62
|
+
}
|
|
63
|
+
return cc
|
|
64
|
+
}
|
|
40
65
|
|
|
41
66
|
// in
|
|
42
67
|
evaluate.operator['in'] = (a,b)=>a in b
|
|
43
|
-
parse.
|
|
68
|
+
parse.operator.splice(6,0,(a,cc,prec,end) => (char(2) === 'in' && [skip(2), '"' + a + '"', expr(prec,end)]))
|
|
44
69
|
|
|
45
70
|
// []
|
|
46
|
-
operator['['] = (...args) => Array(...args)
|
|
47
|
-
token.
|
|
48
|
-
|
|
71
|
+
evaluate.operator['['] = (...args) => Array(...args)
|
|
72
|
+
parse.token.unshift((cc, node, arg) =>
|
|
73
|
+
cc === 91 &&
|
|
49
74
|
(
|
|
50
|
-
|
|
51
|
-
node = arg
|
|
52
|
-
|
|
53
|
-
)
|
|
75
|
+
skip(), arg=expr(0,93),
|
|
76
|
+
node = arg===nil ? ['['] : arg[0] === ',' ? (arg[0]='[',arg) : ['[',arg],
|
|
77
|
+
skip(), node
|
|
78
|
+
)
|
|
54
79
|
)
|
|
55
80
|
|
|
56
81
|
// {}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
operator
|
|
82
|
+
parse.token.unshift((cc, node) => (
|
|
83
|
+
cc === 123 ? (skip(), node = map(['{',expr(0,125)]), skip(), node) : null
|
|
84
|
+
))
|
|
85
|
+
parse.operator.splice(4,0, binary(cc=>cc===58))
|
|
86
|
+
evaluate.operator['{'] = (...args)=>Object.fromEntries(args)
|
|
87
|
+
evaluate.operator[':'] = (a,b)=>[a,b]
|
|
61
88
|
|
|
62
89
|
const map = (n, args) => {
|
|
63
|
-
if (n[1]
|
|
90
|
+
if (n[1]===nil) args = []
|
|
64
91
|
else if (n[1][0]==':') args = [n[1]]
|
|
65
92
|
else if (n[1][0]==',') args = n[1].slice(1)
|
|
66
93
|
return ['{', ...args]
|
|
67
94
|
}
|
|
68
95
|
|
|
69
|
-
|
|
70
96
|
// TODO: strings interpolation
|
|
71
97
|
|
|
72
|
-
export
|
|
98
|
+
export default parse
|
|
73
99
|
export { parse, evaluate }
|
package/justin.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e,r,
|
|
1
|
+
var e,r,o=e=>Array.isArray(e)&&("string"==typeof e[0]||o(e[0])),t=(e,r={},n,p)=>o(e)?("string"==typeof(n=e[0])&&(p=a[n]),"function"!=typeof(n=p||t(n,r))?n:n.call(...e.map((e=>t(e,r))))):e&&"string"==typeof e?'"'===e[0]?e.slice(1,-1):"@"===e[0]?e.slice(1):e in r?r[e]:e:e,a=t.operator={"!":e=>!e,"++":e=>++e,"--":e=>--e,".":(...e)=>e.reduce(((e,r)=>e&&e[r])),"%":(...e)=>e.reduce(((e,r)=>e%r)),"/":(...e)=>e.reduce(((e,r)=>e/r)),"*":(...e)=>e.reduce(((e,r)=>e*r)),"+":(...e)=>e.reduce(((e,r)=>e+r)),"-":(...e)=>e.length<2?-e:e.reduce(((e,r)=>e-r)),">>>":(e,r)=>e>>>r,">>":(e,r)=>e>>r,"<<":(e,r)=>e<<r,">=":(e,r)=>e>=r,">":(e,r)=>e>r,"<=":(e,r)=>e<=r,"<":(e,r)=>e<r,"!=":(e,r)=>e!=r,"==":(e,r)=>e==r,"&":(e,r)=>e&r,"^":(e,r)=>e^r,"|":(e,r)=>e|r,"&&":(...e)=>e.every(Boolean),"||":(...e)=>e.some(Boolean),",":(...e)=>e.reduce(((e,r)=>r))},n=(o,t)=>(r=o,e=0,t=u(),e<r.length?p():t),p=(o="Bad syntax")=>{throw Error(o+" `"+r[e]+"` at "+e)},s=(o=1,t=e)=>{if("number"==typeof o)e+=o;else for(;o(i());)e++;return r.slice(t,e)||c},i=(o=0)=>r.charCodeAt(e+o),l=(o=1)=>r.substr(e,o),c="",u=(r=0,o,t=n.space(),a,s=e,l=0,u)=>{for(;s===e&&l<n.token.length;)a=n.token[l++](t);for(l=Math.max(0|h[t=n.space()],r);l<n.operator.length&&!(t===o||l<r);)(u=n.operator[l++](a,t,l,o))&&(u.indexOf(c)>=0&&p("Bad expression"),a=u,l=Math.max(0|h[t=n.space()],r));return!r&&o&&i()!=o&&p("Unclosed paren"),a},f=(n.space=r=>{for(;(r=i())<=32;)e++;return r},n.token=[e=>(e=s((e=>e>47&&e<58||46===e)))?((69===i()||101===i())&&(e+=s(2)+s((e=>e>=48&&e<=57))),isNaN(e=parseFloat(e))?p("Bad number"):e):c,(e,r)=>34===e?s()+s((r=>r-e))+s():c,r=>40===r?(++e,r=u(0,41),++e,r===c?p():r):c,e=>s((e=>e>=48&&e<=57||e>=65&&e<=90||e>=97&&e<=122||36==e||95==e||e>=192&&215!=e&&247!=e))],e=>(r,o,t,a,p,i,f)=>{if(r!==c&&(i=0|e(o))){p=[f=l(i),r];do{s(i),p.push(u(t,a))}while(n.space()==o&&l(i)==f);return p}}),d=(e,r=!1)=>r?(r,o,t,a,n)=>r!==c&&(n=0|e(o))&&[s(n),r]:(r,o,t,a,n)=>r===c&&(n=0|e(o))&&[s(n),u(t-1,a)],h=(n.operator=[f((e=>44==e)),f((e=>124==e&&i(1)==e&&2)),f((e=>38==e&&i(1)==e&&2)),f((e=>124==e)),f((e=>94==e)),f((e=>38==e)),f((e=>(61==e||33==e)&&61==i(1)&&(i(1)==i(2)?3:2))),f((e=>(62==e||60==e)&&e!=i(1))),f((e=>(60==e||62==e)&&e==i(1)&&(e==i(2)?3:2))),f((e=>(43==e||45==e)&&i(1)!=e)),f((e=>42==e&&42!=i(1)||47==e||37==e)),d((e=>(43==e||45==e)&&i(1)==e&&2),!0),d((e=>(43==e||45==e||33==e)&&(i(1)==e?2:1))),(r,o,t,a,n)=>46==o?[s(),r,"string"==typeof(n=u(t,a))?'"'+n+'"':n]:91==o?(e++,r=[".",r,u(0,93)],e++,r):40==o?(e++,n=u(0,41),e++,Array.isArray(n)&&","===n[0]?(n[0]=r,n):n===c?[r]:[r,n]):c],[]);h[44]=0,h[124]=1,h[38]=2,h[94]=4,h[61]=h[33]=6,h[60]=h[62]=7,h[43]=h[45]=9,h[42]=h[47]=h[37]=10,h[46]=h[91]=h[40]=13,n.operator[0]=f((e=>44==e||59==e)),n.token.splice(3,0,(e=>!(116!==e||"true"!==l(4)||!s(4))||(102!==e||"false"!==l(5)||!s(5))&&(110===e&&"null"===l(4)&&s(4)?null:void(117===e&&"undefined"===l(9)&&s(9))))),n.token[1]=(e,r,o,t)=>{if(34===e||39===e){for(r=l(),s(),t="";(o=i())-e;)92===o?(s(),t+=y[l()]||l()):t+=l(),s();return s(),r+t+r}};var y={n:"\n",r:"\r",t:"\t",b:"\b",f:"\f",v:"\v"};t.operator["**"]=(...e)=>e.reduceRight(((e,r)=>Math.pow(r,e))),n.operator.splice(n.operator.length-3,0,f((e=>42===e&&i(1)===e&&2))),n.operator.splice(n.operator.length-2,0,d((e=>126===e))),t.operator["~"]=e=>~e,t.operator["?:"]=(e,r,o)=>e?r:o,n.operator.splice(1,0,((e,r,o,t)=>{let a,p;if(63===r&&(e===c&&err("Bad expression"),s(),n.space(),a=u(-1,58),58===i()))return s(),n.space(),p=u(),["?:",e,a,p]})),n.space=e=>{for(;(e=i())<=32;)s(),47===i()&&(42===i(1)?(s(2),s((e=>42!==e&&47!==i(1))),s(2)):47===i(1)&&(s(2),s((e=>e>=32))));return e},t.operator.in=(e,r)=>e in r,n.operator.splice(6,0,((e,r,o,t)=>"in"===l(2)&&[s(2),'"'+e+'"',u(o,t)])),t.operator["["]=(...e)=>Array(...e),n.token.unshift(((e,r,o)=>91===e&&(s(),r=(o=u(0,93))===c?["["]:","===o[0]?(o[0]="[",o):["[",o],s(),r))),n.token.unshift(((e,r)=>123===e?(s(),r=g(["{",u(0,125)]),s(),r):null)),n.operator.splice(4,0,f((e=>58===e))),t.operator["{"]=(...e)=>Object.fromEntries(e),t.operator[":"]=(e,r)=>[e,r];var g=(e,r)=>(e[1]===c?r=[]:":"==e[1][0]?r=[e[1]]:","==e[1][0]&&(r=e[1].slice(1)),["{",...r]),v=n;export{v as default,t as evaluate,n as parse};
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "subscript",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Microlanguage with common syntax for JS/C++/Python/Rust",
|
|
5
5
|
"main": "subscript.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
8
|
-
"
|
|
8
|
+
"parse.js",
|
|
9
|
+
"evaluate.js",
|
|
9
10
|
"subscript.js",
|
|
10
11
|
"subscript.min.js",
|
|
11
12
|
"justin.js",
|
package/parse.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// precedence-based parsing
|
|
2
|
+
const GT=62, LT=60, EQ=61, PLUS=43, MINUS=45, AND=38, OR=124, HAT=94, MUL=42, DIV=47, MOD=37, PERIOD=46, OBRACK=91, CBRACK=93, OPAREN=40, CPAREN=41, COMMA=44, SPACE=32, EXCL=33
|
|
3
|
+
|
|
4
|
+
// current string & index
|
|
5
|
+
let idx, cur
|
|
6
|
+
|
|
7
|
+
export const parse = (str, tree) => (cur=str, idx=0, tree=expr(), idx<cur.length ? err() : tree),
|
|
8
|
+
|
|
9
|
+
err = (msg='Bad syntax') => { throw Error(msg + ' `' + cur[idx] + '` at ' + idx) },
|
|
10
|
+
|
|
11
|
+
skip = (is=1, from=idx) => {
|
|
12
|
+
if (typeof is === 'number') idx += is
|
|
13
|
+
else while (is(code())) idx++;
|
|
14
|
+
return cur.slice(from, idx) || nil
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
code = (i=0) => cur.charCodeAt(idx+i),
|
|
18
|
+
|
|
19
|
+
char = (n=1) => cur.substr(idx, n),
|
|
20
|
+
|
|
21
|
+
nil = '',
|
|
22
|
+
|
|
23
|
+
// a + b - c
|
|
24
|
+
expr = (prec=0, end, cc=parse.space(), node, from=idx, i=0, mapped) => {
|
|
25
|
+
// prefix or token
|
|
26
|
+
while (from===idx && i < parse.token.length) node = parse.token[i++](cc)
|
|
27
|
+
|
|
28
|
+
// postfix or binary
|
|
29
|
+
for (i = Math.max(lookup[cc=parse.space()]|0, prec); i < parse.operator.length;) {
|
|
30
|
+
if (cc===end || i<prec) break // if lookup got prec lower than current - end group
|
|
31
|
+
else if (mapped = parse.operator[i++](node, cc, i, end))
|
|
32
|
+
mapped.indexOf(nil) >=0 && err('Bad expression'),
|
|
33
|
+
node = mapped, i = Math.max(lookup[cc=parse.space()]|0, prec); // we pass i+1 as precision
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!prec && end && code()!=end) err('Unclosed paren')
|
|
37
|
+
|
|
38
|
+
return node
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// can be extended with comments, so we export
|
|
42
|
+
space = parse.space = cc => { while (cc = code(), cc <= SPACE) idx++; return cc },
|
|
43
|
+
|
|
44
|
+
// tokens
|
|
45
|
+
token = parse.token = [
|
|
46
|
+
// 1.2e+3, .5 - fast & small version, but consumes corrupted nums as well
|
|
47
|
+
(number) => {
|
|
48
|
+
if (number = skip(c => (c > 47 && c < 58) || c === PERIOD)) {
|
|
49
|
+
if (code() === 69 || code() === 101) number += skip(2) + skip(c => c >= 48 && c <= 57)
|
|
50
|
+
return isNaN(number = parseFloat(number)) ? err('Bad number') : number
|
|
51
|
+
}
|
|
52
|
+
return nil
|
|
53
|
+
},
|
|
54
|
+
// "a"
|
|
55
|
+
(q, qc) => q === 34 ? (skip() + skip(c => c-q) + skip()) : nil,
|
|
56
|
+
// (...exp)
|
|
57
|
+
c => c === OPAREN ? (++idx, c=expr(0,CPAREN), ++idx, c===nil?err():c) : nil,
|
|
58
|
+
// var
|
|
59
|
+
c => skip(c =>
|
|
60
|
+
(c >= 48 && c <= 57) || // 0..9
|
|
61
|
+
(c >= 65 && c <= 90) || // A...Z
|
|
62
|
+
(c >= 97 && c <= 122) || // a...z
|
|
63
|
+
c == 36 || c == 95 || // $, _,
|
|
64
|
+
(c >= 192 && c != 215 && c != 247) // any non-ASCII
|
|
65
|
+
)
|
|
66
|
+
],
|
|
67
|
+
|
|
68
|
+
// create binary operator parser
|
|
69
|
+
binary = (is) => (a,cc,prec,end, list,len,op) => {
|
|
70
|
+
if (a!==nil && (len = is(cc)|0)) {
|
|
71
|
+
// consume same-op group, do..while saves op lookups
|
|
72
|
+
list = [op=char(len),a]
|
|
73
|
+
do { skip(len), list.push(expr(prec,end)) } while (parse.space()==cc && char(len)==op)
|
|
74
|
+
return list
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
unary = (is, post=false) => post ?
|
|
78
|
+
(a,cc,prec,end,l) => a!==nil && (l=is(cc)|0) && [skip(l), a] :
|
|
79
|
+
(a,cc,prec,end,l) => a===nil && (l=is(cc)|0) && [skip(l), expr(prec-1,end)],
|
|
80
|
+
|
|
81
|
+
operator = parse.operator = [
|
|
82
|
+
// ','
|
|
83
|
+
binary(c => c==COMMA),
|
|
84
|
+
// '||' '&&'
|
|
85
|
+
binary(c => c==OR && code(1)==c && 2),
|
|
86
|
+
binary(c => c==AND && code(1)==c && 2),
|
|
87
|
+
// '|' '^' '&'
|
|
88
|
+
binary(c => c==OR),
|
|
89
|
+
binary(c => c==HAT),
|
|
90
|
+
binary(c => c==AND),
|
|
91
|
+
// '==' '!='
|
|
92
|
+
binary(c => (c==EQ || c==EXCL) && code(1)==EQ && (code(1)==code(2) ? 3 : 2)),
|
|
93
|
+
// '<' '>' '<=' '>='
|
|
94
|
+
binary(c => (c==GT || c==LT) && c!=code(1)),
|
|
95
|
+
// '<<' '>>' '>>>'
|
|
96
|
+
binary(c => (c==LT || c==GT) && c==code(1) && (c==code(2) ? 3 : 2)),
|
|
97
|
+
// '+' '-'
|
|
98
|
+
binary(c => (c==PLUS || c==MINUS) && code(1)!=c),
|
|
99
|
+
// '*' '/' '%'
|
|
100
|
+
binary(c => (c==MUL && code(1) != MUL) || c==DIV || c==MOD),
|
|
101
|
+
// -- ++ unaries
|
|
102
|
+
unary(c => (c==PLUS || c==MINUS) && code(1) == c && 2, true),
|
|
103
|
+
// - + ! unaries
|
|
104
|
+
unary(c => (c==PLUS || c==MINUS || c==EXCL) && (code(1)==c ? 2 : 1)),
|
|
105
|
+
// '()', '[]', '.'
|
|
106
|
+
(a,cc,prec,end,b) => (
|
|
107
|
+
// a.b[c](d)
|
|
108
|
+
cc==PERIOD ? [skip(), (a), typeof (b = (expr(prec,end))) === 'string' ? '"' + b + '"' : b] :
|
|
109
|
+
cc==OBRACK ? (idx++, a = ['.', (a), (expr(0,CBRACK))], idx++, a) :
|
|
110
|
+
cc==OPAREN ? (
|
|
111
|
+
idx++, b=expr(0,CPAREN), idx++,
|
|
112
|
+
Array.isArray(b) && b[0]===',' ? (b[0]=a, b) :
|
|
113
|
+
b === nil ? [a] :
|
|
114
|
+
[a, b]
|
|
115
|
+
) : nil
|
|
116
|
+
)
|
|
117
|
+
],
|
|
118
|
+
|
|
119
|
+
// fast operator lookup table
|
|
120
|
+
lookup = []
|
|
121
|
+
|
|
122
|
+
lookup[COMMA] = 0
|
|
123
|
+
lookup[OR] = 1
|
|
124
|
+
lookup[AND] = 2
|
|
125
|
+
lookup[HAT] = 4
|
|
126
|
+
lookup[EQ] = lookup[EXCL] = 6
|
|
127
|
+
lookup[LT] = lookup[GT] = 7
|
|
128
|
+
lookup[PLUS] = lookup[MINUS] = 9
|
|
129
|
+
lookup[MUL] = lookup[DIV] = lookup[MOD] = 10
|
|
130
|
+
lookup[PERIOD] = lookup[OBRACK] = lookup[OPAREN] = 13
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
export default parse
|
package/subscript.js
CHANGED
package/subscript.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e,r,
|
|
1
|
+
var e,r,a=(a,o)=>(r=a,e=0,o=c(),e<r.length?t():o),t=(a="Bad syntax")=>{throw Error(a+" `"+r[e]+"` at "+e)},o=(a=1,t=e)=>{if("number"==typeof a)e+=a;else for(;a(s());)e++;return r.slice(t,e)||p},s=(a=0)=>r.charCodeAt(e+a),n=(a=1)=>r.substr(e,a),p="",c=(r=0,o,n=a.space(),c,i=e,u=0,f)=>{for(;i===e&&u<a.token.length;)c=a.token[u++](n);for(u=Math.max(0|l[n=a.space()],r);u<a.operator.length&&!(n===o||u<r);)(f=a.operator[u++](c,n,u,o))&&(f.indexOf(p)>=0&&t("Bad expression"),c=f,u=Math.max(0|l[n=a.space()],r));return!r&&o&&s()!=o&&t("Unclosed paren"),c},i=(a.space=r=>{for(;(r=s())<=32;)e++;return r},a.token=[e=>(e=o((e=>e>47&&e<58||46===e)))?((69===s()||101===s())&&(e+=o(2)+o((e=>e>=48&&e<=57))),isNaN(e=parseFloat(e))?t("Bad number"):e):p,(e,r)=>34===e?o()+o((r=>r-e))+o():p,r=>40===r?(++e,r=c(0,41),++e,r===p?t():r):p,e=>o((e=>e>=48&&e<=57||e>=65&&e<=90||e>=97&&e<=122||36==e||95==e||e>=192&&215!=e&&247!=e))],e=>(r,t,s,i,u,l,f)=>{if(r!==p&&(l=0|e(t))){u=[f=n(l),r];do{o(l),u.push(c(s,i))}while(a.space()==t&&n(l)==f);return u}}),u=(e,r=!1)=>r?(r,a,t,s,n)=>r!==p&&(n=0|e(a))&&[o(n),r]:(r,a,t,s,n)=>r===p&&(n=0|e(a))&&[o(n),c(t-1,s)],l=(a.operator=[i((e=>44==e)),i((e=>124==e&&s(1)==e&&2)),i((e=>38==e&&s(1)==e&&2)),i((e=>124==e)),i((e=>94==e)),i((e=>38==e)),i((e=>(61==e||33==e)&&61==s(1)&&(s(1)==s(2)?3:2))),i((e=>(62==e||60==e)&&e!=s(1))),i((e=>(60==e||62==e)&&e==s(1)&&(e==s(2)?3:2))),i((e=>(43==e||45==e)&&s(1)!=e)),i((e=>42==e&&42!=s(1)||47==e||37==e)),u((e=>(43==e||45==e)&&s(1)==e&&2),!0),u((e=>(43==e||45==e||33==e)&&(s(1)==e?2:1))),(r,a,t,s,n)=>46==a?[o(),r,"string"==typeof(n=c(t,s))?'"'+n+'"':n]:91==a?(e++,r=[".",r,c(0,93)],e++,r):40==a?(e++,n=c(0,41),e++,Array.isArray(n)&&","===n[0]?(n[0]=r,n):n===p?[r]:[r,n]):p],[]);l[44]=0,l[124]=1,l[38]=2,l[94]=4,l[61]=l[33]=6,l[60]=l[62]=7,l[43]=l[45]=9,l[42]=l[47]=l[37]=10,l[46]=l[91]=l[40]=13;var f=a,d=e=>Array.isArray(e)&&("string"==typeof e[0]||d(e[0])),y=(e,r={},a,t)=>d(e)?("string"==typeof(a=e[0])&&(t=h[a]),"function"!=typeof(a=t||y(a,r))?a:a.call(...e.map((e=>y(e,r))))):e&&"string"==typeof e?'"'===e[0]?e.slice(1,-1):"@"===e[0]?e.slice(1):e in r?r[e]:e:e,h=y.operator={"!":e=>!e,"++":e=>++e,"--":e=>--e,".":(...e)=>e.reduce(((e,r)=>e&&e[r])),"%":(...e)=>e.reduce(((e,r)=>e%r)),"/":(...e)=>e.reduce(((e,r)=>e/r)),"*":(...e)=>e.reduce(((e,r)=>e*r)),"+":(...e)=>e.reduce(((e,r)=>e+r)),"-":(...e)=>e.length<2?-e:e.reduce(((e,r)=>e-r)),">>>":(e,r)=>e>>>r,">>":(e,r)=>e>>r,"<<":(e,r)=>e<<r,">=":(e,r)=>e>=r,">":(e,r)=>e>r,"<=":(e,r)=>e<=r,"<":(e,r)=>e<r,"!=":(e,r)=>e!=r,"==":(e,r)=>e==r,"&":(e,r)=>e&r,"^":(e,r)=>e^r,"|":(e,r)=>e|r,"&&":(...e)=>e.every(Boolean),"||":(...e)=>e.some(Boolean),",":(...e)=>e.reduce(((e,r)=>r))},g=y,m=e=>(e="string"==typeof e?f(e):e,r=>g(e,r));export{m as default,g as evaluate,f as parse};
|
package/src/parse.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
const PERIOD = 46, OPAREN = 40, CPAREN = 41, OBRACK = 91, CBRACK = 93, PLUS = 43, MINUS = 45
|
|
2
|
-
|
|
3
|
-
export let index, current, lastOp
|
|
4
|
-
|
|
5
|
-
export const parse = str => (current=str, index=lastOp=0, expr()),
|
|
6
|
-
|
|
7
|
-
space = () => { while (code() <= 32) index++ },
|
|
8
|
-
|
|
9
|
-
// consume operator that resides within current group by precedence
|
|
10
|
-
operator = (ops, op, prec, l=3) => {
|
|
11
|
-
// memoize by index - saves 20% to perf
|
|
12
|
-
if (index && lastOp && lastOp[3] === index) return lastOp
|
|
13
|
-
|
|
14
|
-
// ascending lookup is faster 1-char operators, longer for 2+ char ops
|
|
15
|
-
while (l) if ((prec=ops[op=char(l--)])!=null) return lastOp = [op, prec, op.length, index] //opinfo
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
expr = (end, prec=-1) => {
|
|
19
|
-
space()
|
|
20
|
-
|
|
21
|
-
let cc = code(), op, node, i=0, mapped, from=index
|
|
22
|
-
|
|
23
|
-
if (cc === end) return
|
|
24
|
-
|
|
25
|
-
// parse node by token parsers (direct loop is faster than stack entry)
|
|
26
|
-
while (from===index && i < token.length) node = token[i++](cc)
|
|
27
|
-
|
|
28
|
-
// unary prefix
|
|
29
|
-
if (from===index) (op = operator(unary)) && (index += op[2], node = [op[0], expr(end, op[1])])
|
|
30
|
-
|
|
31
|
-
// postfix handlers allow a.b[c](d).e, postfix operators, literals etc.
|
|
32
|
-
else {
|
|
33
|
-
if (space(), cc=code(), cc === end) return node
|
|
34
|
-
for (i=0; i < postfix.length;) if ((mapped=postfix[i](node, cc)) !== node) node=mapped, i=0, space(), cc=code(); else i++
|
|
35
|
-
}
|
|
36
|
-
// else do {space(), cc=code()} while (postfix.find((parse, mapped) => (mapped = parse(node, cc)) !== node && (node = mapped)))
|
|
37
|
-
|
|
38
|
-
space()
|
|
39
|
-
|
|
40
|
-
// consume expression for current precedence or group (== highest precedence)
|
|
41
|
-
while ((cc = code()) && cc !== end && (op = operator(binary)) && op[1] > prec) {
|
|
42
|
-
node = [op[0], node]
|
|
43
|
-
// consume same-op group, do..while both saves op lookups and space
|
|
44
|
-
do { index += op[2], node.push(expr(end, op[1])) } while (char(op[2]) === op[0])
|
|
45
|
-
space()
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return node;
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
// tokens
|
|
52
|
-
// 1.2e+3, .5
|
|
53
|
-
float = (number, c, isDigit) => {
|
|
54
|
-
number = next(isDigit = c => c >= 48 && c <= 57) || ''
|
|
55
|
-
if (code() === PERIOD) index++, number += '.' + next(isDigit)
|
|
56
|
-
if (number) {
|
|
57
|
-
if ((c = code()) === 69 || c === 101) { // e, E
|
|
58
|
-
index++, number += 'e'
|
|
59
|
-
if ((c=code()) === PLUS || c === MINUS) // +-
|
|
60
|
-
number += char(), index++
|
|
61
|
-
number += next(isDigit)
|
|
62
|
-
}
|
|
63
|
-
return parseFloat(number)
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
// "a"
|
|
68
|
-
string = (q, qc) => q === 34 ? (qc = char(), index++, qc) + next(c => c !== q) + (index++, qc) : null,
|
|
69
|
-
|
|
70
|
-
// (...exp)
|
|
71
|
-
group = (c, node) => c === OPAREN ? (index++, node = expr(CPAREN), index++, node) : null,
|
|
72
|
-
|
|
73
|
-
id = name => (name = next(c =>
|
|
74
|
-
(c >= 48 && c <= 57) || // 0..9
|
|
75
|
-
(c >= 65 && c <= 90) || // A...Z
|
|
76
|
-
(c >= 97 && c <= 122) || // a...z
|
|
77
|
-
c == 36 || c == 95 || // $, _,
|
|
78
|
-
c >= 192 // any non-ASCII
|
|
79
|
-
)) && literal.hasOwnProperty(name) ? literal[name] : name,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// ------------ util
|
|
83
|
-
code = () => current.charCodeAt(index), // current char code
|
|
84
|
-
char = (n=1) => current.substr(index, n), // next n chars
|
|
85
|
-
err = (msg) => { throw Error(msg + ' at ' + index) },
|
|
86
|
-
next = (is=1, from=index) => { // number indicates skip & stop (don't check)
|
|
87
|
-
if (typeof is === 'number') index += is
|
|
88
|
-
else while (is(code())) ++index > current.length && err('Unexpected end ' + is) // 1 + true === 2;
|
|
89
|
-
return index > from ? current.slice(from, index) : null
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// ----------- config
|
|
94
|
-
token = parse.token = [ group, float, string, id ],
|
|
95
|
-
|
|
96
|
-
literal = parse.literal = {true:true, false:false, null:null},
|
|
97
|
-
|
|
98
|
-
postfix = parse.postfix = [
|
|
99
|
-
// a.b[c](d), 3 in 1 for performance
|
|
100
|
-
(node, cc, arg) => {
|
|
101
|
-
if (cc === PERIOD) index++, space(), node = ['.', node, '"'+id()+'"']
|
|
102
|
-
else if (cc === OBRACK) index++, node = ['.', node, expr(CBRACK)], index++
|
|
103
|
-
else if (cc === OPAREN)
|
|
104
|
-
index++, arg=expr(CPAREN),
|
|
105
|
-
node = Array.isArray(arg) && arg[0]===',' ? (arg[0]=node, arg) : arg == null ? [node] : [node, arg],
|
|
106
|
-
index++
|
|
107
|
-
return node
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
// a++, a--
|
|
111
|
-
(node, cc) => (cc===0x2b || cc===0x2d) && current.charCodeAt(index+1)===cc ? [next(2), node] : node,
|
|
112
|
-
],
|
|
113
|
-
|
|
114
|
-
unary = parse.unary = {
|
|
115
|
-
'-': 17,
|
|
116
|
-
'!': 17,
|
|
117
|
-
'+': 17,
|
|
118
|
-
'++': 17,
|
|
119
|
-
'--': 17
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
binary = parse.binary = {
|
|
123
|
-
',': 1,
|
|
124
|
-
'||': 6, '&&': 7, '|': 8, '^': 9, '&': 10,
|
|
125
|
-
'==': 11, '!=': 11,
|
|
126
|
-
'<': 12, '>': 12, '<=': 12, '>=': 12,
|
|
127
|
-
'<<': 13, '>>': 13, '>>>': 13,
|
|
128
|
-
'+': 14, '-': 14,
|
|
129
|
-
'*': 15, '/': 15, '%': 15
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export default parse
|