assemblyscript
assemblyscript copied to clipboard
Support overflow checks of arithmetic operations
Most modern compilers like Rust and Swift support special set arithmetic operations which cause to exceptions when result is overflow or underflow.
-
In
Swiftall integer operations checked by default but support special operations like&+,&-and&*for unchecked operations. -
Rusthas much more clever approach. In Debug mode all operations overflow checked but in release all kind of operations are wrapped if not specify flags like-Z force-overflow-checks. In the same timeRustprovide wide range builtin methods for handling overflow behaviour per operation like:a.wrapping_add(b)(allow overflow),a.saturating_add(b)(likeclamp(res, i32::MIN, i32::MAX)),a.overflowing_add(b)(return(i32, bool)tuple) anda.checked_add(b)(returnOption<i32>)
So I propose add similar feature for AssemblyScript in this way:
Unchecked (wrapped/overflowed) operations (default for now)
a + b
a - b
a * b
a / b
a % b
a++
--a
!a
Checked operations which produce unreachable exception in runtime during overflow:
a !+ b
a !- b
a !* b
a !/ b
a !% b
a!++
--a!
!a // <-- need figure out how handle this. Hmm, may be `!a!`?
Why !+, !* and etc?
First of all it has great semantic look ! which mean warning, exception and exclusion. Secondary symbol ! on TS after variables is legit and ignore during transpiling except case when a: i32 | null. When a define as optional type in my opinion we could write something like this a! !+ b which also legit.
Why this important?
This protect code from bugs. This especially important for smart contracts and history of Ethereum/EVM knows many examples when ignoring overflow costs a lot of money.
WDYT?
I don't think typescript tooling will like these operators. It will cause people to have to deal with errors in their problems tab.
Maybe we can have a checked(expression) macro?
Another suggestion might be a Checked<Number> type.
Yes checked may be an option. But produce more verbosity.
Checked<i32> is bad idea because this cause to a lot of problems when we try to mix checked type with unchecked. Should we cast all unchecked types to unchecked? Implicitly or explicitly? In both cases this produce more verbosity and makes refactoring difficulty. All languages resolve this in operation level not type level
Right. I was thinking the same thing. How about a grouped postfix !. Like this: (a + b)! ?
I think that's valid typescript too.
yes, this is valid as well.
btw one more cons to checked. We already have unchecked for fast bound uncheck access to array's buffer so mixed a[i] = unchecked(checked(b + c)) will confuse
Using "!" for non null assertions and checked expressions is gonna be confusing too. I'm just at a loss for good options at this point. We are very limited by the typescript compiler.
Makes sense to me. Would prefer checked and unchecked contexts for this similar to C#, and maybe rename our current unchecked to something else, maybe unsafe?
Once there are closures, something like this could also work
checked(() => {
...
...
...
});
by tagging the entire inner function context.
Yeah. C# uses checked(a + b) for that stuff. In my opinion much better than a.checked_add(b) because much easier for refactoring but still verbosity. Ideally is something like in Swift &+, &* ... but this not valid for typescript unfortunately.
Also good suggestion from @jtenner:
(a + b)!, (a / b)!
Another funny (magic?) variant =)
a +~~ b
a -~~ b
a *~~ b
a /~~ b
a !~~ b
~~a++
--~~a
!~~a
(a + b)!, (a / b)!
Not sure about reusing ! for something different than its semantic meaning. That was the reason for not merging non-null assertions initially, because I didn't know what we'd like to do with it instead, but then came to the conclusion that doing it the same way TS does it makes the most sense. Might be that there are ways to make it work in multiple ways, but I still think that developer experience would suffer from having to deal with one operator with multiple semantics.
How about comments like @ts-ignore?
EDIT: This means that operation validation would be on the statement level instead of on the expression level.
// @as-validate(: Reason)
// @as-overflow-checks(: Reason)
Another suggestion might be block level checks like this:
// choose an intuitive block name here
as_validate: {
// this block generates overflow checks
}
Also note that reused block names are valid javascript, so there shouldn't be any issues with this approach.
The label idea looks nice, but might of course conflict with labels at some point. Hmm...
@MaxGraey @dcodeIO I think it's better to use different data types vs different operator syntax. Having to use different operator makes it easy to make accidental errors. Data types can enforce for which values it's important to have overflow checks and enforce it throughout all code using them, even if it's generic library code.
So e.g. you can always use safe_u64 when dealing with security-sensitive code (e.g. handling money amounts in smart contracts) and use raw u64 when performance needs to be optimized (probably most browser use cases).
Honestly, implementing safe data types would be awesome. Please consider implementing a u8_clamped data type backed by i32 which would make Uint8ClampedArray very easy to implement.
It was mentioned that it would get confusing dealing with number types. I say we force explicit casts between safe and unsafe number types to "opt in" to safe operations.
var a: safe_i32 = 6000;
var b: safe_i32 = a + <safe_i32>2147483647; // throws
var c: i32 = 300;
var d = a + c; // compile time error: Invalid math operation, explicit cast required to add unsafe/safe numbers
This is not confusing at all. We already have to do casting to add numbers of different types all the time.
Format suggestions:
u8clamped
[type]safe
safe_[type]
Safe<type>
Clamped<type>
I won't discount macros for expression level safety either. This is my personal suggestion.
SAFE(expression);
I won't discount macros for expression level safety either. This is my personal suggestion.
Probably safer other way around: UNSAFE(expr).
I was commenting on the idea of using a macro. Not the name :)
what i've came up with is the type name i32c standing for "checked". when either in operands of a binop is checked, it results in the checked type.
(or i32w for deterministic warping behavior, i32 for overflow-checked, that's the if-im-going-make-a-friendly-language idea in my head)
btw what about f32c f64c trapping Infinity and NaN?