Merge branch 'master' of github.com:AleoHQ/leo into bug/abnf-parser-bugs

This commit is contained in:
gluaxspeed 2021-07-09 11:55:10 -07:00
commit d4d122e59e
19 changed files with 252 additions and 84 deletions

View File

@ -30,8 +30,6 @@ jobs:
- name: cargo fmt --check
uses: actions-rs/cargo@v1
env:
CARGO_NET_GIT_FETCH_WITH_CLI: true
with:
command: fmt
args: --all -- --check
@ -120,7 +118,6 @@ jobs:
command: test
args: --all --features ci_skip
env:
CARGO_NET_GIT_FETCH_WITH_CLI: true
CARGO_INCREMENTAL: "0"
- name: Install dependencies for code coverage

View File

@ -23,7 +23,7 @@ pub struct ConstantFolding<'a, 'b> {
}
impl<'a, 'b> ExpressionVisitor<'a> for ConstantFolding<'a, 'b> {
fn visit_expression(&mut self, input: &Cell<&Expression<'a>>) -> VisitResult {
fn visit_expression(&mut self, input: &Cell<&'a Expression<'a>>) -> VisitResult {
let expr = input.get();
if let Some(const_value) = expr.const_value() {
let folded_expr = Expression::Constant(Constant {

View File

@ -14,8 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
use crate::{AsgConvertError, IntegerType, Span, Type};
use crate::{AsgConvertError, Circuit, Identifier, IntegerType, Span, Type};
use indexmap::IndexMap;
use num_bigint::BigInt;
use std::{convert::TryInto, fmt};
use tendril::StrTendril;
@ -118,8 +119,8 @@ impl From<leo_ast::CharValue> for CharValue {
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ConstValue {
#[derive(Clone, PartialEq)]
pub enum ConstValue<'a> {
Int(ConstInt),
Group(GroupValue),
Field(BigInt),
@ -128,8 +129,9 @@ pub enum ConstValue {
Char(CharValue),
// compounds
Tuple(Vec<ConstValue>),
Array(Vec<ConstValue>),
Tuple(Vec<ConstValue<'a>>),
Array(Vec<ConstValue<'a>>),
Circuit(&'a Circuit<'a>, IndexMap<String, (Identifier, ConstValue<'a>)>),
}
macro_rules! const_int_op {
@ -311,8 +313,8 @@ impl ConstInt {
}
}
impl ConstValue {
pub fn get_type<'a>(&self) -> Option<Type<'a>> {
impl<'a> ConstValue<'a> {
pub fn get_type(&'a self) -> Option<Type<'a>> {
Some(match self {
ConstValue::Int(i) => i.get_type(),
ConstValue::Group(_) => Type::Group,
@ -324,6 +326,7 @@ impl ConstValue {
Type::Tuple(sub_consts.iter().map(|x| x.get_type()).collect::<Option<Vec<Type>>>()?)
}
ConstValue::Array(values) => Type::Array(Box::new(values.get(0)?.get_type()?), values.len()),
ConstValue::Circuit(circuit, _) => Type::Circuit(circuit),
})
}

View File

@ -58,7 +58,7 @@ impl<'a> ExpressionNode<'a> for ArrayAccessExpression<'a> {
self.array.get().is_mut_ref()
}
fn const_value(&self) -> Option<ConstValue> {
fn const_value(&self) -> Option<ConstValue<'a>> {
let mut array = match self.array.get().const_value()? {
ConstValue::Array(values) => values,
_ => return None,

View File

@ -53,7 +53,7 @@ impl<'a> ExpressionNode<'a> for ArrayInitExpression<'a> {
false
}
fn const_value(&self) -> Option<ConstValue> {
fn const_value(&self) -> Option<ConstValue<'a>> {
let element = self.element.get().const_value()?;
Some(ConstValue::Array(vec![element; self.len]))
}

View File

@ -78,7 +78,7 @@ impl<'a> ExpressionNode<'a> for ArrayInlineExpression<'a> {
false
}
fn const_value(&self) -> Option<ConstValue> {
fn const_value(&self) -> Option<ConstValue<'a>> {
let mut const_values = vec![];
for (expr, spread) in self.elements.iter() {
if *spread {

View File

@ -70,7 +70,7 @@ impl<'a> ExpressionNode<'a> for ArrayRangeAccessExpression<'a> {
self.array.get().is_mut_ref()
}
fn const_value(&self) -> Option<ConstValue> {
fn const_value(&self) -> Option<ConstValue<'a>> {
let mut array = match self.array.get().const_value()? {
ConstValue::Array(values) => values,
_ => return None,
@ -176,6 +176,7 @@ impl<'a> FromAst<'a, leo_ast::ArrayRangeAccessExpression> for ArrayRangeAccessEx
} else {
None
};
if let Some(expected_len) = expected_len {
if let Some(length) = length {
if length != expected_len {

View File

@ -83,8 +83,14 @@ impl<'a> ExpressionNode<'a> for CircuitAccessExpression<'a> {
}
}
fn const_value(&self) -> Option<ConstValue> {
None
fn const_value(&self) -> Option<ConstValue<'a>> {
match self.target.get()?.const_value()? {
ConstValue::Circuit(_, members) => {
let (_, const_value) = members.get(&self.member.name.to_string())?.clone();
Some(const_value)
}
_ => None,
}
}
fn is_consty(&self) -> bool {

View File

@ -70,8 +70,17 @@ impl<'a> ExpressionNode<'a> for CircuitInitExpression<'a> {
true
}
fn const_value(&self) -> Option<ConstValue> {
None
fn const_value(&self) -> Option<ConstValue<'a>> {
let mut members = IndexMap::new();
for (identifier, member) in self.values.iter() {
// insert by name because accessmembers identifiers are different.
members.insert(
identifier.name.to_string(),
(identifier.clone(), member.get().const_value()?),
);
}
// Store circuit as well for get_type.
Some(ConstValue::Circuit(self.circuit.get(), members))
}
fn is_consty(&self) -> bool {

View File

@ -36,7 +36,7 @@ use std::cell::Cell;
pub struct Constant<'a> {
pub parent: Cell<Option<&'a Expression<'a>>>,
pub span: Option<Span>,
pub value: ConstValue, // should not be compound constants
pub value: ConstValue<'a>, // should not be compound constants
}
impl<'a> Node for Constant<'a> {
@ -56,7 +56,7 @@ impl<'a> ExpressionNode<'a> for Constant<'a> {
fn enforce_parents(&self, _expr: &'a Expression<'a>) {}
fn get_type(&self) -> Option<Type<'a>> {
fn get_type(&'a self) -> Option<Type<'a>> {
self.value.get_type()
}
@ -64,7 +64,7 @@ impl<'a> ExpressionNode<'a> for Constant<'a> {
false
}
fn const_value(&self) -> Option<ConstValue> {
fn const_value(&self) -> Option<ConstValue<'a>> {
Some(self.value.clone())
}
@ -267,6 +267,7 @@ impl<'a> Into<leo_ast::ValueExpression> for &Constant<'a> {
),
ConstValue::Tuple(_) => unimplemented!(),
ConstValue::Array(_) => unimplemented!(),
ConstValue::Circuit(_, _) => unimplemented!(),
}
}
}

View File

@ -124,9 +124,9 @@ pub trait ExpressionNode<'a>: Node {
fn get_parent(&self) -> Option<&'a Expression<'a>>;
fn enforce_parents(&self, expr: &'a Expression<'a>);
fn get_type(&self) -> Option<Type<'a>>;
fn get_type(&'a self) -> Option<Type<'a>>;
fn is_mut_ref(&self) -> bool;
fn const_value(&self) -> Option<ConstValue>; // todo: memoize
fn const_value(&'a self) -> Option<ConstValue>; // todo: memoize
fn is_consty(&self) -> bool;
}
@ -194,7 +194,7 @@ impl<'a> ExpressionNode<'a> for Expression<'a> {
}
}
fn get_type(&self) -> Option<Type<'a>> {
fn get_type(&'a self) -> Option<Type<'a>> {
use Expression::*;
match self {
VariableRef(x) => x.get_type(),
@ -236,7 +236,7 @@ impl<'a> ExpressionNode<'a> for Expression<'a> {
}
}
fn const_value(&self) -> Option<ConstValue> {
fn const_value(&'a self) -> Option<ConstValue<'a>> {
use Expression::*;
match self {
VariableRef(x) => x.const_value(),

View File

@ -56,7 +56,7 @@ impl<'a> ExpressionNode<'a> for TernaryExpression<'a> {
self.if_true.get().is_mut_ref() && self.if_false.get().is_mut_ref()
}
fn const_value(&self) -> Option<ConstValue> {
fn const_value(&self) -> Option<ConstValue<'a>> {
if let Some(ConstValue::Boolean(switch)) = self.condition.get().const_value() {
if switch {
self.if_true.get().const_value()

View File

@ -56,7 +56,7 @@ impl<'a> ExpressionNode<'a> for TupleAccessExpression<'a> {
self.tuple_ref.get().is_mut_ref()
}
fn const_value(&self) -> Option<ConstValue> {
fn const_value(&self) -> Option<ConstValue<'a>> {
let tuple_const = self.tuple_ref.get().const_value()?;
match tuple_const {
ConstValue::Tuple(sub_consts) => sub_consts.get(self.index).cloned(),

View File

@ -58,7 +58,7 @@ impl<'a> ExpressionNode<'a> for TupleInitExpression<'a> {
false
}
fn const_value(&self) -> Option<ConstValue> {
fn const_value(&self) -> Option<ConstValue<'a>> {
let mut consts = vec![];
for element in self.elements.iter() {
if let Some(const_value) = element.get().const_value() {

View File

@ -66,7 +66,7 @@ impl<'a> ExpressionNode<'a> for VariableRef<'a> {
}
// todo: we can use use hacky ssa here to catch more cases, or just enforce ssa before asg generation finished
fn const_value(&self) -> Option<ConstValue> {
fn const_value(&self) -> Option<ConstValue<'a>> {
let variable = self.variable.borrow();
if variable.mutable || variable.assignments.len() != 1 {
return None;

View File

@ -23,7 +23,7 @@ use crate::{
program::ConstrainedProgram,
relational::*,
resolve_core_circuit,
value::{Address, Char, CharType, ConstrainedValue, Integer},
value::{Address, Char, CharType, ConstrainedCircuitMember, ConstrainedValue, Integer},
FieldType,
GroupType,
};
@ -37,7 +37,7 @@ impl<'a, F: PrimeField, G: GroupType<F>> ConstrainedProgram<'a, F, G> {
pub(crate) fn enforce_const_value<CS: ConstraintSystem<F>>(
&mut self,
cs: &mut CS,
value: &ConstValue,
value: &'a ConstValue<'a>,
span: &Span,
) -> Result<ConstrainedValue<'a, F, G>, ExpressionError> {
Ok(match value {
@ -75,6 +75,17 @@ impl<'a, F: PrimeField, G: GroupType<F>> ConstrainedProgram<'a, F, G> {
.map(|x| self.enforce_const_value(cs, x, span))
.collect::<Result<Vec<_>, _>>()?,
),
ConstValue::Circuit(circuit, members) => {
let mut constrained_members = Vec::new();
for (_, (identifier, member)) in members.iter() {
constrained_members.push(ConstrainedCircuitMember(
identifier.clone(),
self.enforce_const_value(cs, member, span)?,
));
}
ConstrainedValue::CircuitExpression(circuit, constrained_members)
}
})
}

View File

@ -19,12 +19,9 @@ DRAFT
This proposal provides support for casts among integer types in Leo.
The syntax is similar to Rust.
Two possible semantics are discussed:
_value-preserving casts_,
which just serve to change types
but cause errors when values are not representable in the new types;
and _values-changing casts_,
which never cause errors but may change the mathematical values.
The semantics is _value-preserving_,
i.e. the casts just serve to change types
but cause errors when the mathematical values are not representable in the new types.
# Motivation
@ -33,7 +30,7 @@ arithmetic integer operations require operands of the same type
and return results of the same type.
There are no implicit or explicit ways to turn, for example,
a `u8` into a `u16`, even though
every non-negative integers that fits in 8 bits also fits in 16 bits.
every non-negative integer that fits in 8 bits also fits in 16 bits.
However, the ability to convert values between different (integer) types
is a useful feature that is normally found in programming languages.
@ -68,7 +65,7 @@ The proposed syntax is
```
where `<expression>` must have an integer type.
The ABNF grammar is modified as follows:
The ABNF grammar of Leo is modified as follows:
```
; add this rule:
cast-expression = unary-expression
@ -112,21 +109,33 @@ but the value in question is in the intersection of the two ranges.
When the mathematical integer value of the expression
is not representable in the type that the expression is cast to,
there are two possible approaches, discussed below.
there are two possible approaches:
_value-preserving casts_,
which just serve to change types
but cause errors when values are not representable in the new types;
and _values-changing casts_,
which never cause errors but may change the mathematical values.
### Value-Preserving Casts
Based on discussion and consensus within the Leo team,
this RFC proposes value-preserving casts;
value-changing casts are discussed in the 'Alternatives' section,
for completeness.
The first approach is to deem that situation erroneous.
That is, to require casts to always preserve the mathematical integer values.
With value-preserving casts,
when the mathematical integer value of the expression
is not representable in the type that the expression is cast to,
it is an error.
That is, we require casts to always preserve the mathematical integer values.
Recall that all inputs are known at compile time in Leo,
so these checks can be performed easily.
In this approach, casts only serve to change types, never values.
Thus integer casts only serve to change types, never values.
When values are to be changed, separate (built-in) functions can be used,
e.g. to mask bits and achieve the same effect as
the value-changing casts discussed below.
From a point of view, this approach seems to match Leo's
treatment of potentially erroneous situations like integer overflows:
the principle is that developers should explicitly use
This approach Leo's treatment of potentially erroneous situations like integer overflows.
The principle is that developers should explicitly use
operations that may overflow if that is their intention,
rather than having those situation possibly occur unexpectedly.
@ -159,9 +168,32 @@ let r_low16: u32 = r & 0xFFFF; // assuming we have bitwise ops and hex literals
let s: u16 = r_low16 as u16; // no value change here
```
### Value-Changing Casts
## Compilation to R1CS
The second approach is the following:
It may be more efficient (in terms of number of R1CS constraints)
to compile Leo casts as if they had a value-changing semantics.
If the R1CS constraints represent Leo integers as bits,
the bits of the new value can be determined from the bits of the old value,
with additional zero or sign extension bits when needed
(see the details of the value-changing semantics in the 'Alternatives' section).
There is no need to add checks to the R1CS constraints
because the compiler ensures that the cast values do not actually change given the known inputs,
and therefore the value-changing and value-preserving semantics are equivalent on the known inputs.
The idea is that the R1CS constraints can have a "don't care" behavior on inputs that cause errors in Leo.
# Drawbacks
This proposal does not appear to bring any drawbacks,
other than making the language and compiler inevitably more complex.
But the benefits to support type casts justifies the extra complexity.
# Effect on Ecosystem
This proposal does not appear to have any direct effects on the ecosystem.
# Alternatives
As mentioned above, an alternative semantics for casts is value-changing:
1. `uN` to `uM` with `N < M`: just change type of value.
2. `uN` to `uM` with `N > M`: take low `M` bits of value.
3. `iN` to `iM` with `N < M`: just change type of value.
@ -185,7 +217,7 @@ considerations that apply to typical programming languages
do not necessarily apply to Leo.
Back to the somewhat abstract example in the section on value-preserving casts,
with value-changing casts, the expectation that the final result fits in `u16`
note that, with value-changing casts, the expectation that the final result fits in `u16`
would have to be checked with explicit code:
```
... // some computations on u32 values, which could not be done with u16
@ -197,35 +229,3 @@ let s: u16 = r as u16; // could change value in principle, but does not here
```
However, it would be easy for a developer to neglect to add the checking code,
and thus have the Leo code silently produce an unexpected result.
## Compilation to R1CS
It should be possible to compile Leo casts to the same R1CS constraints
whether we choose the value-preserving or value-changing semantics.
If the R1CS constraints represent Leo integers as bits,
the bits of the new value can be determined from the bits of the old value,
with additional zero or sign extension bits when needed.
This is clearly the value-changing behavior.
With the value-preserving behavior,
all casts for the known inputs are checked,
and thus we value-changing behavior coincides
with the value-preserving behavior if the checks succeed.
Thus, if he behavior of the R1CS constraints is "don't care"
for Leo program inputs that cause errors (such as cast errors),
the compilation strategy for value-changing casts
should be also adequate for value-preserving casts.
# Drawbacks
This proposal does not appear to bring any drawbacks,
other than making the language and compiler inevitably more complex.
But the benefits to support type casts justifies the extra complexity.
# Effect on Ecosystem
This proposal does not appear to have any direct effects on the ecosystem.
# Alternatives
The 'Design' section above already discusses two alternative semantics.
After we settle on one, the other one could be mentioned in this section.

View File

@ -0,0 +1,135 @@
# Leo RFC 005: Countdown Loops
## Authors
- Max Bruce
- Collin Chin
- Alessandro Coglio
- Eric McCarthy
- Jon Pavlik
- Damir Shamanaev
- Damon Sicore
- Howard Wu
## Status
DRAFT
# Summary
This proposal suggests adding countdown loops and inclusive loop ranges into Leo language.
# Motivation
In the current design of the language only incremental ranges are allowed. Though
in some cases there's a need for loops going in the reverse direction. This example
demonstrates the shaker sort algorithm where countdown loops are mocked:
```ts
function shaker_sort(a: [u32; 10], const rounds: u32) -> [u32; 10] {
for k in 0..rounds {
for i in 0..9 {
if a[i] > a[i + 1] {
let tmp = a[i];
a[i] = a[i + 1];
a[i + 1] = tmp;
}
}
for j in 0..9 { // j goes from 0 to 8
let i = 8 - j; // j is flipped
if a[i] > a[i + 1] {
let tmp = a[i];
a[i] = a[i + 1];
a[i + 1] = tmp;
}
}
}
return a;
}
```
Having a countdown loop in the example above could improve readability and
usability of the language by making it more natural to the developer.
However, if we imagined this example using a countdown loop, we would see that
it wouldn't be possible to count to 0; because the first bound of the range is
inclusive and the second is exclusive, and loops ranges must use only unsigned integers.
```ts
// loop goes 0,1,2,3,4,5,6,7,8
for i in 0..9 { /* ... */ }
// loop goes 9,8,7,6,5,4,3,2,1
for i in 9..0 { /* ... */ }
```
Hence direct implementation of the coundown loop ranges would create asymmetry (1)
and would not allow loops to count down to 0 (2). To implement coundown loops and
solve these two problems we suggest adding an inclusive range bounds.
# Design
## Coundown loops
Countdown ranges do not need any changes to the existing syntax. However their
functionality needs to be implemented in the compiler.
```ts
for i in 5..0 {}
```
## Inclusive ranges
To solve loop asymmetry and to improve loop ranges in general we suggest adding
inclusive range operator to Leo. Inclusive range would extend the second bound
of the loop making it inclusive (instead of default - exclusive)
therefore allowing countdown loops to reach 0 value.
```ts
// default loop: 0,1,2,3,4
for i in 0..5 {}
// inclusive range: 0,1,2,3,4,5
for i in 0..=5 {}
```
## Example
The code example demostrated in the Motivation part of this document
could be extended (or simplified) with the suggested syntax:
```ts
function shaker_sort(a: [u32; 10], const rounds: u32) -> [u32; 10] {
for k in 0..rounds {
for i in 0..9 { // i goes from 0 to 8
if a[i] > a[i + 1] {
let tmp = a[i];
a[i] = a[i + 1];
a[i + 1] = tmp;
}
}
for i in 8..=0 { // i goes from 8 to 0
if a[i] > a[i + 1] {
let tmp = a[i];
a[i] = a[i + 1];
a[i + 1] = tmp;
}
}
}
return a;
}
```
# Drawbacks
-
# Effect on Ecosystem
Suggested change should have no effect on ecosystem because of its backward compatibility.
# Alternatives
Coundown loops can be mocked manually.

View File

@ -5,6 +5,10 @@ input_file:
- input/complex_access.in
*/
circuit Circ {
f: u32
}
function main (a: [u8; 8], b: u32, c: [[u8; 3]; 3], d: [(u8, u32); 1], e: [u8; (3, 4)] ) -> bool {
a[0..3][b] = 93;
a[2..6][1] = 87;
@ -16,6 +20,7 @@ function main (a: [u8; 8], b: u32, c: [[u8; 3]; 3], d: [(u8, u32); 1], e: [u8; (
c[0..2][0] = [1u8; 3];
c[1..][1][1..2][0] = 126;
c[1..][0] = [42, 43, 44];
c[Circ {f: 0}.f..1][0][0] += 2;
d[..1][0].1 = 1;
@ -24,7 +29,7 @@ function main (a: [u8; 8], b: u32, c: [[u8; 3]; 3], d: [(u8, u32); 1], e: [u8; (
return
a == [200u8, 93, 42, 174, 5, 6, 43, 8]
&& c == [[1u8, 1, 1], [42, 43, 44], [7, 126, 9]]
&& c == [[3u8, 1, 1], [42, 43, 44], [7, 126, 9]]
&& d == [(0u8, 1u32)]
&& e == [[33u8, 22, 22, 22], [0, 0, 0, 0], [0, 0, 0, 0]];
}