-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JS arbex implementation: parse, typecheck, compile expressions #4841
Conversation
e72044a
to
ea2521b
Compare
72237fa
to
38f0058
Compare
@jfirebaugh @ansis plenty of work still left to do here, but a base implementation is in place in case you want to take an early look. |
637f4af
to
0a272d4
Compare
name: 'object', | ||
type: lambda(ObjectType, ValueType), | ||
compile: fromContext('asObject') | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
json_array
and object
are assertions (yielding an error if the input isn't the right type), whereas number
, string
, boolean
are coercions. Should we also have assertions for number/string/boolean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool stuff! I didn't thoroughly review everything, but wanted to submit what I noticed so far.
}; | ||
} | ||
|
||
function defineBinaryMathOp(name, isAssociative) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of the current uses of defineBinaryMathOp
pass a second argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, whoops! This was intended for allowing +
, &&
, etc. to take > 2 args as a convenience.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 2b91368
}; | ||
} | ||
|
||
function defineComparisonOp(name, isAssociative) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto defineComparisonOp
.
} | ||
*/ | ||
|
||
const expressions: { [string]: Definition } = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[ExpressionName]: Definition
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack. Seems like this isn't compatible with accessing this map using values pulled from a mixed
array. There's no good way to take myRawJSONExpression[0]
and refine it so that flow knows that it's an ExpressionName
.
}, | ||
|
||
typeOf: function (x) { | ||
if (Array.isArray(x)) return 'Vector<Value>'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the only place generics syntax is exposed publicly? I wonder if Array
is a better public name for this type, for familiarity. Internally we could rename Vector<T>
⇢ Array<T>
and Array<T, N>
⇢ FixedArray<T, N>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not quite the only place it's exposed publicly: type checking error messages will need to refer to types by name, and probably some runtime error messages will, too. That said, I'm also with you in that I've been uneasy/unsatisfied with having Vector<Value>
as a possible output of typeof
(because of both the generics syntax and the Value
alias).
Would it be confusing for [typeof, [get, 'foo']]
to equal Array
, but then to have [number, [at, [get, 'foo'], 0]]
to produce a runtime error message like Expected Vector<number> but properties.foo was an object instead
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could always print names like:
- Value:
JSONValue
- Vector:
T[]
- Array<T, N>:
T[N]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, if we think T[]
is too concise and easy to misread, we could do:
- Vector:
Array of T's
- Array<T, N>:
Array of N T's
const result = []; | ||
while (args.length > 1) { | ||
const c = args.splice(0, 2); | ||
result.push(`${c[0].js} ? ${c[1].js}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parentheses are needed around subexpressions, either in individual compile
definitions or inserted globally by the compiler. Else you'll get the wrong precedence when compiling things like ["&&", a, ["case", b, c, d]]
-- desired precedence is a && (b ? c : d)
, but without parentheses a && b ? c : d
parses as (a && b) ? c : d
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 yep -- it's done globally at the end of compile()
, 04dbb97#diff-442324b3174c25e635f18536d7732df2R147, but this reminds me, we should include a test that targets this since it's be very easy to regress
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 013a59e
src/style-spec/function/compile.js
Outdated
* | ||
* @private | ||
*/ | ||
function compileExpression(expr: any) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like it's possible to narrow any
to mixed
here without too much trouble.
src/style-spec/function/compile.js
Outdated
isFeatureConstant: boolean, | ||
isZoomConstant: boolean, | ||
expression: TypedExpression, | ||
function?: any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This any
can be Function
.
2d49f8e
to
69ca97a
Compare
For consistency with |
c98700d
to
834a6c6
Compare
docs/style-spec/expressions.md
Outdated
@@ -134,7 +134,7 @@ Every expression evaluates to a value of one of the following types. | |||
###Decision: | |||
- `["case", cond1: Boolean, result1: T, cond2: Boolean, result2: T, ..., cond_m, result_m: T, result_otherwise: T] -> T` | |||
- `["match", x: T, a_1: T, y_1: U, a_2: T, y_2: U, ..., a_m: T, y_m: U, y_else: U]` - `a_1`, `a_2`, ... must be _literal_ values of type `T`. | |||
- `["is_error", expr: T]` - `true` if `expr` is an `Error` value, `false` otherwise | |||
- `["coalesce", e1: T, e2: T, e3: T, ...] -> T` - evaluates each expression in turn until the first non-error value is obtained, and returns that value. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be "non-error and non-null"
8d1e797
to
d2692a0
Compare
If a user specifies a property function with, say, numeric stop outputs, for a style property whose spec definition specifies a string, we accept the mistyped function and just yield the default. IMO, this should be a validation error instead. |
dd09152
to
10cec45
Compare
10cec45
to
ca9d55d
Compare
When I look at real world examples like in #4836, I want to be able to extract array and object literals as constants, assigned in If/when we do support this for both objects and arrays, they should be parallel in syntax. Here are some possibilities:
|
Funny, I have the opposite instinct. I would think assertion/annotation to be the more common (especially if we expect Studio to auto-insert annotations based on tilestats), and coercion to be the more dangerous, and hence better suited to the less concise name. |
'string': { | ||
name: 'string', | ||
type: lambda(StringType, ValueType), | ||
compile: args => ({ js: `String(${args[0].js})` }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it wise to inherit all of JS's coercion quirks?
["string", ["^", 2, 70]]
⇢ "1.1805916207174113e+21"["string", ["properties"]]
⇢ "[object Object]"["string", ["array", 1, 2]]
⇢ "1,2"["string, ["get", "weird"]]
+{"properties": {"weird": {"toString": "asdf"}}
⇢ TypeError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think it seems reasonable to lean on the platform's number->string conversion... as for objects and arrays... would it be reasonable to just punt, requiring primitive types only?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 👍
Good point -- I was thinking more that coercion would be more common in manually-authored expressions, which is why I was going for conciseness...
... but this is convincing. 👍 for making coercion less concise / more explicit. |
This was my thought as well. Runtime styling doesn’t have the benefit of auto-annotation via tilestats. Per mapbox/mapbox-gl-native#8074 (comment), I don’t think developers on iOS will mind a little danger if they can avoid mandatory type annotations (which would have major usability issues). |
c289e5f
to
b75efe5
Compare
074b108
to
8f9f2df
Compare
b75efe5
to
c1c29cb
Compare
cc10399
to
ab4c1fb
Compare
} | ||
type = (!itemtype || itemtype === 'Value') ? | ||
'Array' : `Array<${titlecase(itemtype)}>`; | ||
const t = ArrayLiteral.inferArrayType(items); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jfirebaugh This change causes the runtime typing of [get, foo]
when foo
is an array to be as specific as possible, based on its actual contents:
- the array is homogenous of type number, string, or boolean, then =>
Array<typeof item, properties.foo.length>
- otherwise =>
Array<Value, properties.foo.length>
|
||
return array(itemType || ValueType, value.length); | ||
} | ||
if (!isValue(args[0])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of now, there's actually no way to express a color literal -- rgba
and color
are functions -- but this would allow ['literal', new Color(...)]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I figured that would be okay -- since we're not likely to export Color
publicly, there no way to construct such an expression. And even if there were, it'd be harmless to do so.
@@ -145,12 +145,12 @@ function parseExpression(expr: mixed, context: ParsingContext) : Expression { | |||
return new LiteralExpression(key, NumberType, expr); | |||
} else if (Array.isArray(expr)) { | |||
if (expr.length === 0) { | |||
throw new ParsingError(key, `Expected an array with at least one element.`); | |||
throw new ParsingError(key, `Expected an array with at least one element. If you wanted a literal array, use ["literal", []].`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 to this and the others like it.
Now that we have an AST made up of
|
💯 I'm doing this as part of implementation of |
0fd95a5
to
f1b8ceb
Compare
Few notes/questions about
|
f1b8ceb
to
10eb188
Compare
constructor(key: string, name: string, type: Type) { | ||
super(key, type); | ||
if (!/^[a-zA-Z_]+[a-zA-Z_0-9]*$/.test(name)) | ||
throw new ParsingError(key, `Invalid identifier ${name}.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check should be moved to LetExpression.parse()
, and it should also either check against JS reserved words or else 'escape' them (e.g. replace function
with var_function
or something)
b0203b0
to
19c47dc
Compare
3a2fab2
to
6ba399f
Compare
Squashed this down, FF merging into |
Todo:
match
expressions (blocked by syntax design Project Arbex (Arbitrary Expressions) #4777 (comment))curve
expressionscolor
expressionsget
,has
to be more concise per draft of expressions examples doc #4836 (comment)Vector<T>
andArray<T, N>
coalesce
zoom
expression, and add stop/interpolation metadata to compiled resultlet
Type checking a(punt on this for now)number->number
againstnumber->variant<number|string>
should yield an expression of typenumber->number
.{expression: [...]}
as a function.(globalProperties, feature)
rather than(globalProperties, featureProperties)
(needed forgeometry_type
andid
)setPaintProperty
, etc.) -- but see JS arbex implementation: parse, typecheck, compile expressions #4841 (comment); we should look into what native does in this case.=>
(and any other es6 stuff) from generated code, since it won't be transpiled.expressions.md
to accurately reflect the implementation here.Implement(doing this in a followup)cubic-bezier
interpolation type forcurve
expressions