Skip to content

Commit

Permalink
[compiler-v2] Add loop labels to the language (#14868)
Browse files Browse the repository at this point in the history
* [compiler-v2] Add loop labels to the language

Besides the user being able to describe more complex algorithms more efficiently, loop labels are required to express any reducible control flow in the AST language, and create parity of the AST with the bytecode level for this kind of code (which is also what can be generated from Move).

* Apply suggestions from code review

* Addressing reviewer comments.
  • Loading branch information
wrwg authored Oct 11, 2024
1 parent a822e65 commit 55e368c
Show file tree
Hide file tree
Showing 29 changed files with 500 additions and 61 deletions.
7 changes: 2 additions & 5 deletions third_party/move/move-compiler-v2/src/bytecode_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,11 +450,11 @@ impl<'env> Generator<'env> {
self.emit_with(*id, |attr| Bytecode::Jump(attr, continue_label));
self.emit_with(*id, |attr| Bytecode::Label(attr, break_label));
},
ExpData::LoopCont(id, 0, do_continue) => {
ExpData::LoopCont(id, nest, do_continue) => {
if let Some(LoopContext {
continue_label,
break_label,
}) = self.loops.last()
}) = self.loops.iter().rev().nth(*nest)
{
let target = if *do_continue {
*continue_label
Expand All @@ -466,9 +466,6 @@ impl<'env> Generator<'env> {
self.error(*id, "missing enclosing loop statement")
}
},
ExpData::LoopCont(_, _, _) => {
unimplemented!("continue/break with nesting")
},
ExpData::SpecBlock(id, spec) => {
// Map locals in spec to assigned temporaries.
let mut replacer = |id, target| {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// -- Model dump before bytecode pipeline
module 0x815::test {
private fun f1() {
loop {
loop {
loop {
if true {
loop {
if false {
continue[3]
} else {
break[1]
};
break
}
} else {
continue[2]
}
}
};
break
}
}
} // end 0x815::test

// -- Sourcified model before bytecode pipeline
module 0x815::test {
fun f1() {
'l0: loop {
loop 'l1: loop if (true) loop {
if (false) continue 'l0 else break 'l1;
break
} else continue 'l0;
break
}
}
}

============ initial bytecode ================

[variant baseline]
fun test::f1() {
var $t0: bool
var $t1: bool
0: label L0
1: label L2
2: label L4
3: $t0 := true
4: if ($t0) goto 5 else goto 19
5: label L6
6: label L9
7: $t1 := false
8: if ($t1) goto 9 else goto 12
9: label L11
10: goto 0
11: goto 14
12: label L12
13: goto 23
14: label L13
15: goto 17
16: goto 6
17: label L10
18: goto 21
19: label L7
20: goto 0
21: label L8
22: goto 2
23: label L5
24: goto 1
25: label L3
26: goto 28
27: goto 0
28: label L1
29: return ()
}


============ bytecode verification succeeded ========
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module 0x815::test {
fun f1() {
'outer: loop {
// unlabeled loop, but counts in nesting in AST
loop {
'inner: loop if (true) loop {
if (false) continue 'outer else break 'inner;
break
} else continue 'outer
};
break
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Diagnostics:
error: unsupported language construct
┌─ tests/checking-lang-v1/loop_labels.move:3:9
3 │ 'outer: loop {
│ ^^^^^^ loop labels are not enabled before version 2.1

error: unsupported language construct
┌─ tests/checking-lang-v1/loop_labels.move:6:17
6 │ 'inner: loop if (true) loop {
│ ^^^^^^ loop labels are not enabled before version 2.1

error: unsupported language construct
┌─ tests/checking-lang-v1/loop_labels.move:7:41
7 │ if (false) continue 'outer else break 'inner;
│ ^^^^^^ loop labels are not enabled before version 2.1

error: unsupported language construct
┌─ tests/checking-lang-v1/loop_labels.move:7:59
7 │ if (false) continue 'outer else break 'inner;
│ ^^^^^^ loop labels are not enabled before version 2.1

error: unsupported language construct
┌─ tests/checking-lang-v1/loop_labels.move:9:33
9 │ } else continue 'outer
│ ^^^^^^ loop labels are not enabled before version 2.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module 0x815::test {
fun f1() {
'outer: loop {
// unlabeled loop, but counts in nesting in AST
loop {
'inner: loop if (true) loop {
if (false) continue 'outer else break 'inner;
break
} else continue 'outer
};
break
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

Diagnostics:
error: label `'outer` undefined
┌─ tests/checking/control_flow/loop_labels_check_err.move:3:15
3 │ break 'outer;
│ ^^^^^^

error: label `'inner` undefined
┌─ tests/checking/control_flow/loop_labels_check_err.move:5:19
5 │ break 'inner
│ ^^^^^^

error: label `'l1` already used by outer loop
┌─ tests/checking/control_flow/loop_labels_check_err.move:11:19
11 │ 'l1: loop 'l1: loop {};
│ --- ^^^
│ │
│ outer definition of label
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module 0x815::test {
fun undefined_label() {
break 'outer;
'outer: loop {
break 'inner
}
}

fun duplicate_label() {
'l1: loop {};
'l1: loop 'l1: loop {};
'l1: loop {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// -- Model dump before bytecode pipeline
module 0x815::test {
private fun f1() {
loop {
loop {
loop {
if true {
loop {
if false {
continue[3]
} else {
break[1]
};
break
}
} else {
continue[2]
}
}
};
break
}
}
} // end 0x815::test

// -- Sourcified model before bytecode pipeline
module 0x815::test {
fun f1() {
'l0: loop {
loop 'l1: loop if (true) loop {
if (false) continue 'l0 else break 'l1;
break
} else continue 'l0;
break
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module 0x815::test {
fun f1() {
'outer: loop {
// unlabeled loop, but counts in nesting in AST
loop {
'inner: loop if (true) loop {
if (false) continue 'outer else break 'inner;
break
} else continue 'outer
};
break
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

Diagnostics:
error: unexpected token
┌─ tests/checking/control_flow/loop_labels_parse_err1.move:3:13
3 │ 'a: if (true) false else true
│ ^^
│ │
│ Unexpected 'if'
│ Expected one of: `while` or `loop`
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module 0x815::test {
fun f1(): bool {
'a: if (true) false else true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

Diagnostics:
error: unexpected token
┌─ tests/checking/control_flow/loop_labels_parse_err2.move:3:13
3 │ 'a: if (true) false else true
│ ^^
│ │
│ Unexpected 'if'
│ Expected one of: `while` or `loop`
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module 0x815::test {
fun f1(): bool {
'a: if (true) false else true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

Diagnostics:
error: invalid character
┌─ tests/checking/control_flow/loop_labels_parse_err3.move:3:10
3 │ ': if (true) false else true
│ ^ Label quote must be followed by 'A-Z', `a-z', or '_'
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module 0x815::test {
fun f1(): bool {
': if (true) false else true
}

fun f1(): bool {
'0x: if (true) false else true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

Diagnostics:
error: invalid character
┌─ tests/checking/control_flow/loop_labels_parse_err4.move:3:10
3 │ '0x: if (true) false else true
│ ^ Label quote must be followed by 'A-Z', `a-z', or '_'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module 0x815::test {
fun f1(): bool {
'0x: if (true) false else true
}
}
4 changes: 2 additions & 2 deletions third_party/move/move-compiler-v2/tests/testsuite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const TEST_CONFIGS: Lazy<BTreeMap<&str, TestConfig>> = Lazy::new(|| {
// Turn optimization on by default. Some configs below may turn it off.
.set_experiment(Experiment::OPTIMIZE, true)
.set_experiment(Experiment::OPTIMIZE_WAITING_FOR_COMPARE_TESTS, true)
.set_language_version(LanguageVersion::V2_0);
.set_language_version(LanguageVersion::V2_1);
opts.testing = true;
let configs = vec![
// --- Tests for checking and ast processing
Expand Down Expand Up @@ -718,7 +718,7 @@ const TEST_CONFIGS: Lazy<BTreeMap<&str, TestConfig>> = Lazy::new(|| {
include: vec!["/op-equal/"],
exclude: vec![],
exp_suffix: None,
options: opts.clone().set_language_version(LanguageVersion::V2_1),
options: opts.clone(),
// Run the entire compiler pipeline to double-check the result
stop_after: StopAfter::FileFormat,
dump_ast: DumpLevel::EndStage,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
processed 1 task
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//# run
script {
fun main() {
let result = 0;
'outer: while (result < 100) {
while (result < 50) {
'inner: while (result < 30) {
result += 1;
continue 'outer
};
result += 10;
continue 'outer
};
result += 20
};
assert!(result == 110);
}
}
Loading

0 comments on commit 55e368c

Please sign in to comment.