-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Revise approach to evaluation-time type errors #5263
Conversation
ee39cb1
to
8c949ca
Compare
if (c) return c; | ||
} else if (Array.isArray(input)) { | ||
error = this._validateRGBA(input[0], input[1], input[2], input[3]); | ||
if (!error) return this.rgba(input[0], input[1], input[2], input[3]); |
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 looks redundant because this.rgba
does the same validation check.
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.
Oops, thanks.
if (!c) | ||
throw new RuntimeError(`Could not parse color from value '${input}'`); | ||
cached = this._parseColorCache[input] = new Color(...c); | ||
cached = this._parseColorCache[input] = c ? new Color(...c) : null; |
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.
spread arguments with new alert
const num = Number(value); | ||
if (isNaN(num)) continue; | ||
return num; | ||
} |
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 there any way to avoid relying on arguments
in a runtime-called method? Perhaps we could cache the possible values in an array like we do for stop outputs (and get rid of this method in the eval context)? Let's check and maybe benchmark to be sure it's faster.
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 call, will try that out.
@mourner btw, I was going off of the rule of thumb that using arguments
is fine if you never use the object itself, only access arguments.length
and arguments[i]
-- what's your take on that?
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.
@mourner fixed -- perf was equivalent, this way seems cleaner/better anyway
return defaultValue; | ||
} | ||
return val; | ||
} catch (e) { |
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 there any way we could avoid a full try/catch
here? Maybe passing an errors
array to the compiled function and accumulating errors there somehow?
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.
Definitely possible, but I think it's outside the scope of this PR -- I experimented with a non-try catch approach recently, and it's definitely a nontrivial change. The naive approach (checking for errors before evaluating any expression) was slower than try/catch, and the non-naive approach will take some careful work.
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.
@anandthakker I'm not sure how much exactly it affects performance (I guess it depends on browser engine/version), but as a rule of thumb, I avoid arguments
keyword altogether in perf-sensitive code paths. At least it is easier to make sure a certain function is monomorphic.
ee88053
to
953015b
Compare
ensure(typeof a === 'undefined' || | ||
(a >= 0 && a <= 1), `Invalid rgba value [${[r, g, b, a || 1].join(', ')}]: 'a' must be between 0 and 1.`); | ||
const error = this._validateRGBA(r, g, b, a); | ||
if (error) throw new RuntimeError(error); | ||
return new Color(r / 255, g / 255, b / 255, a); | ||
}, |
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 method still used anywhere?
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, it's used for evaluating the ["rgba", r, g, b, a]
(and "rgb"
) expression
if (c) return c; | ||
} else if (Array.isArray(input)) { | ||
error = this._validateRGBA(input[0], input[1], input[2], input[3]); | ||
if (!error) return new Color(input[0] / 255, input[1] / 255, input[2] / 255, input[3]); |
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.
Can you replace those 2 lines with a call to rgba
?
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.
No, because rgba
throws a RuntimeError
on failure.
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.
Oh, nevermind — I just realized toColor
now acts like coalesce through multiple args...
@anandthakker for type assertions and coercions, is |
@nickidlugash for assertions, For coercions, it depends. |
"error": "Expected a primitive value in [\"string\", ...], but found Object instead." | ||
} | ||
] | ||
"outputs": ["1", "false", "null", "[1,2]", "{\"y\":1}"] |
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 think at one point we decided we didn't want to commit to using a JSON.stringify
representation for to-string
of arrays and objects for portability reasons. Are you thinking differently now?
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 am, yeah. It feels to me like to-string
should always work, and since, with the exception of Color
, all the values in our system can be represented in JSON just fine, I think it seems fine to commit to JSON.stringify
. (Once Color
s can be contained within objects and arrays, we can use the replacer
parameter of JSON.stringify
to deal with those.)
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 don't think we want to commit to having a string representation of every type we ever add to the expression type system. What, for example, would be the result of ["to-string", ["image", "foo"]]
(#5261)?
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.
Eh, I think it's okay to come up with some (obviously lossy) way to stringify Image
values -- {"image": "foo"}
or something -- but even if not, I'd still be in favor of being as liberal as possible. If nothing else, I could imagine it being super convenient while debugging to set a symbol's text-field property to ["to-string", ["get", "offset"]]
(when we finally have support for array values in feature properties)
return String(value); | ||
} else if (value instanceof Color) { | ||
const [r, g, b, a] = value.value; | ||
return `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${a})`; |
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 there a test for this?
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.
Added in bae93ca
var result = ${ctx.compileAndCache(this.args[i])}; | ||
if (result !== null) return result; | ||
} catch (e) {}`); | ||
for (let i = 0; i < this.args.length; i++) { |
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.
for arg of this.args
}, | ||
|
||
coalesce(args: Array<Function>) { | ||
for (let i = 0; i < args.length - 1; i++) { |
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.
Could be written as:
let result = null;
for (const arg of args) {
result = arg();
if (result !== null) break;
}
return result;
Which handles the empty array case. (coalesce
requires one argument currently, but we could consider relaxing this.)
@@ -208,6 +235,15 @@ module.exports = () => ({ | |||
} | |||
|
|||
return interpolate[resultType](outputLower, outputUpper, t); | |||
}, | |||
|
|||
coalesce(args: Array<Function>) { |
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.
Function
→ ()=>Value
'to-boolean': [ BooleanType, [ValueType], ([v]) => `Boolean(${v})` ], | ||
'to-rgba': [ array(NumberType, 4), [ColorType], ([v]) => `${v}.value` ], | ||
'to-color': [ ColorType, [ValueType], fromContext('toColor') ], | ||
'to-number': [ NumberType, varargs(ValueType), (ctx, args) => { |
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-number
and to-color
now do not necessarily evaluate all their arguments -- i.e. they are special forms. Let's write them as such. I think this will also allow you to avoid all the other ctx
and cache
changes to this file.
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.
Another reason these ctx
/ cache
changes may be useful is that they'll let us experiment with inlining arguments (rather than always caching each one as a function eN() { ... }
)
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.
Moved to-number
and to-color
to a special form, Coercion
. If/when it becomes possible for to-string
to fail, we can move it there, too.
9826552
to
e7c60bd
Compare
function wrapForType(expected: Type, expression: Expression, context: ParsingContext) { | ||
const wrapper = typeWrappers[expected.kind]; | ||
if (!wrapper) { | ||
if (expected.kind === '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.
These conditionals largely repeat the conditionals at the wrapForType
callsite. Inline?
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.
👍 did so.
expected.kind === 'String' || | ||
expected.kind === 'Boolean' | ||
) { | ||
// workaround for circular dependency |
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.
Can directly require('./definitions/assertion')
now, right? Same for to-color
if it's a made a special form.
return num; | ||
toNumber: function(args: Array<()=>Value>) { | ||
let value; | ||
for (let i = 0; i < args.length; i++) { |
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.
for...of
@@ -22,7 +22,9 @@ | |||
[0, 1, 0, 1], | |||
[0, 1, 0, 1], | |||
[0, 1, 0, 1], | |||
{"error": "Could not parse color from value '[0,255]'"} | |||
{ | |||
"error": "Invalid rgba value [0, 255, ]: 'r', 'g', and 'b' must be between 0 and 255." |
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 error is misleading -- the issue is the array length, not its contents.
} else { | ||
type = typeOf(value); | ||
typeError = checkSubtype(expectedType, type); | ||
asJSType: function (expectedType: string, values: Array<Function>) { |
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.
Function
→ () => Value
const inputType = this.inputType.kind !== 'Array' ? | ||
`$this.types.${this.inputType.kind}` : JSON.stringify(this.inputType); | ||
|
||
return `(${lookupObject}[$this.as(${input}, ${inputType})] || ${ctx.addExpression(this.otherwise.compile(ctx))})();`; |
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.
What was the purpose of using $this.as
here before?
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 believe it was always redundant, since type checking should already ensure that the input value is the right type.
@@ -107,15 +107,12 @@ class Match implements Expression { | |||
for (let i = 0; i < labels.length; i++) { | |||
if (i > 0) lookup += ', '; | |||
const label = labels[i]; | |||
lookup += `${String(label)}: ${outputs[this.cases[label]]}`; | |||
lookup += `${JSON.stringify(label)}: ${outputs[this.cases[label]]}`; |
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.
Why JSON.stringify
rather than String
? The input is going to implicitly go through String
in ${lookupObject}[${input}]
, so if they behave differently that would seem to be an issue.
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.
The JSON.stringify()
here is to get a JS string literal for the key in the JS object literal we're building. If not, then label === "foo-bar"
would result in invalid code { foo-bar: output }
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 JSON.stringify(number)
is equivalent to String(number)
, so that should be fine. For strings, U+2028 and U+2029 need special handling.
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.
Oy. We'll need to handle those in Literal
, too, then.
Less magic, more explicit
33b5444
to
0308e41
Compare
Closes #5234
Changes:
get
now returnsnull
for a missing property value, rather than throwing an errorcoalesce
now does NOT handle type errors -- it only coalescesnull
string
,number
,boolean
) now accept any number of arguments -- each one is tested in turn until the first value of the desired type is found. If the final value doesn't match, then an error is thrown.to-number
andto-color
work the same way as the assertions, accepting any number of arguments and attempting to coerce each one in turn until one workscc @jfirebaugh @mourner