Skip to content

Add local variables, closes #2752 #8740

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

Merged
merged 27 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eb24734
working impl of local variables for rust generator and interpreter
codeshaunted Jun 20, 2025
e231a99
add discriminator to fix cpp generator
codeshaunted Jun 20, 2025
a8d509b
add type annotation support
codeshaunted Jun 20, 2025
b0cb227
update test
codeshaunted Jun 20, 2025
d415eaa
update docs
codeshaunted Jun 20, 2025
495224a
fix mistake in docs
codeshaunted Jun 20, 2025
19c20ea
add formatter
codeshaunted Jun 20, 2025
1890fd5
make local redeclaration an error, prefix locals with "local_"
codeshaunted Jun 23, 2025
25d1041
add extra check for following identifier for let statement in parser
codeshaunted Jun 23, 2025
10321f7
add extra resolving error for overlapping global names, add syntax test
codeshaunted Jun 23, 2025
90f837e
rework tests, add interpreter test
codeshaunted Jun 23, 2025
f55a551
move docs
codeshaunted Jun 23, 2025
c4f70fe
improve tests
codeshaunted Jun 23, 2025
5dd1814
remove unused import
codeshaunted Jun 23, 2025
3082bed
adjust semantic tokens and add fmt test
codeshaunted Jun 23, 2025
4608667
update syntax tests with extra case
codeshaunted Jun 23, 2025
5fa86c2
add scoping and fix const propogation bug
codeshaunted Jun 24, 2025
84fd545
update comment on let test
codeshaunted Jun 24, 2025
dff5f5d
add unscoped completion for locals
codeshaunted Jun 25, 2025
3063032
disable completion for out of scope and undeclared locals
codeshaunted Jun 25, 2025
edb0529
fix warning in cpp generator
codeshaunted Jun 25, 2025
0192f40
use new component syntax in syntax test
codeshaunted Jun 25, 2025
06774d9
remove overlapping global name check
codeshaunted Jun 25, 2025
83a738e
cleanup pass
codeshaunted Jun 25, 2025
febbd07
clippy fixes
codeshaunted Jun 25, 2025
d1993da
formatting pass
codeshaunted Jun 25, 2025
1160572
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,38 @@ export component Example inherits Window {

## Statements

Assignment:
### Let statements (local variables)
The `let` keyword can be used to create local variables. Local variables are immutable and cannot be redeclared
(even in other scopes). They optionally have a type annotation.
```slint no-test
clicked => {
let foo = "hello world"; // no type annotation, inferred type
debug(foo); // prints "hello world"

let bar: int = 2; // explicit type annotation
debug(bar); // prints "2"
}
```

### Assignment

```slint no-test
clicked => { some-property = 42; }
```

Self-assignment with `+=` `-=` `*=` `/=`
### Self-assignment with `+=` `-=` `*=` `/=`

```slint no-test
clicked => { some-property += 42; }
```

Calling a callback
### Calling a callback

```slint no-test
clicked => { root.some-callback(); }
```

Conditional statements
### Conditional statements

```slint no-test
clicked => {
Expand All @@ -101,7 +114,7 @@ clicked => {
}
```

Empty expression
### Empty expression

```slint no-test
clicked => { }
Expand Down
2 changes: 1 addition & 1 deletion internal/compiler/generator/cpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3130,7 +3130,7 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String
}
Expression::FunctionParameterReference { index, .. } => format!("arg_{index}"),
Expression::StoreLocalVariable { name, value } => {
format!("auto {} = {};", ident(name), compile_expression(value, ctx))
format!("[[maybe_unused]] auto {} = {};", ident(name), compile_expression(value, ctx))
}
Expression::ReadLocalVariable { name, .. } => ident(name).to_string(),
Expression::StructFieldAccess { base, name } => match base.ty(ctx) {
Expand Down
44 changes: 38 additions & 6 deletions internal/compiler/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub struct LookupCtx<'a> {

/// The token currently processed
pub current_token: Option<NodeOrToken>,

/// A stack of local variable scopes
pub local_variables: Vec<Vec<(SmolStr, Type)>>,
}

impl<'a> LookupCtx<'a> {
Expand All @@ -62,6 +65,7 @@ impl<'a> LookupCtx<'a> {
type_register,
type_loader: None,
current_token: None,
local_variables: Default::default(),
}
}

Expand Down Expand Up @@ -218,6 +222,28 @@ impl LookupObject for LookupResult {
}
}

struct LocalVariableLookup;
impl LookupObject for LocalVariableLookup {
fn for_each_entry<R>(
&self,
ctx: &LookupCtx,
f: &mut impl FnMut(&SmolStr, LookupResult) -> Option<R>,
) -> Option<R> {
for scope in ctx.local_variables.iter() {
for (name, ty) in scope {
if let Some(r) = f(
// we need to strip the "local_" prefix because a lookup call will not include it
&name.strip_prefix("local_").unwrap_or(name).into(),
Expression::ReadLocalVariable { name: name.clone(), ty: ty.clone() }.into(),
) {
return Some(r);
}
}
}
None
}
}

struct ArgumentsLookup;
impl LookupObject for ArgumentsLookup {
fn for_each_entry<R>(
Expand Down Expand Up @@ -830,16 +856,22 @@ impl LookupObject for BuiltinNamespaceLookup {

pub fn global_lookup() -> impl LookupObject {
(
ArgumentsLookup,
LocalVariableLookup,
(
SpecialIdLookup,
ArgumentsLookup,
(
IdLookup,
SpecialIdLookup,
(
InScopeLookup,
IdLookup,
(
LookupType,
(BuiltinNamespaceLookup, (ReturnTypeSpecificLookup, BuiltinFunctionLookup)),
InScopeLookup,
(
LookupType,
(
BuiltinNamespaceLookup,
(ReturnTypeSpecificLookup, BuiltinFunctionLookup),
),
),
),
),
),
Expand Down
3 changes: 2 additions & 1 deletion internal/compiler/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,8 @@ declare_syntax! {
/// the right-hand-side of a binding
// Fixme: the test should be a or
BindingExpression-> [ ?CodeBlock, ?Expression ],
CodeBlock-> [ *Expression, *ReturnStatement ],
CodeBlock-> [ *Expression, *LetStatement, *ReturnStatement ],
LetStatement -> [ DeclaredIdentifier, ?Type, Expression ],
ReturnStatement -> [ ?Expression ],
// FIXME: the test should test that as alternative rather than several of them (but it can also be a literal)
Expression-> [ ?Expression, ?FunctionCallExpression, ?IndexExpression, ?SelfAssignment,
Expand Down
34 changes: 34 additions & 0 deletions internal/compiler/parser/statements.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

use crate::parser::r#type::parse_type;

use super::element::parse_code_block;
use super::expressions::parse_expression;
use super::prelude::*;
Expand All @@ -14,6 +16,9 @@ use super::prelude::*;
/// if (true) { foo = bar; } else { bar = foo; }
/// return;
/// if (true) { return 42; }
/// let foo = 1;
/// let bar = foo;
/// let str: string = "hello world";
/// ```
pub fn parse_statement(p: &mut impl Parser) -> bool {
if p.nth(0).kind() == SyntaxKind::RBrace {
Expand Down Expand Up @@ -50,6 +55,11 @@ pub fn parse_statement(p: &mut impl Parser) -> bool {
return true;
}

if p.peek().as_str() == "let" && p.nth(1).kind() == SyntaxKind::Identifier {
parse_let_statement(p);
return true;
}

parse_expression(p);
if matches!(
p.nth(0).kind(),
Expand All @@ -67,6 +77,30 @@ pub fn parse_statement(p: &mut impl Parser) -> bool {
p.test(SyntaxKind::Semicolon)
}

#[cfg_attr(test, parser_test)]
/// ```test,LetStatement
/// let foo = 1;
/// let bar = foo;
/// let str: string = "hello world";
/// ```
fn parse_let_statement(p: &mut impl Parser) {
let mut p = p.start_node(SyntaxKind::LetStatement);
debug_assert_eq!(p.peek().as_str(), "let");
p.expect(SyntaxKind::Identifier); // "let"
{
let mut p = p.start_node(SyntaxKind::DeclaredIdentifier);
p.expect(SyntaxKind::Identifier);
}

if p.test(SyntaxKind::Colon) {
parse_type(&mut *p);
}

p.expect(SyntaxKind::Equal);
parse_expression(&mut *p);
p.expect(SyntaxKind::Semicolon);
}

#[cfg_attr(test, parser_test)]
/// ```test,ConditionalExpression
/// if (true) { foo = bar; } else { bar = foo; }
Expand Down
5 changes: 4 additions & 1 deletion internal/compiler/passes/const_propagation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ fn simplify_expression(expr: &mut Expression) -> bool {
};
can_inline
}
Expression::CodeBlock(stmts) if stmts.len() == 1 => {
// disable this simplification for store local variable, as "let" is not an expression in rust
Expression::CodeBlock(stmts)
if stmts.len() == 1 && !matches!(stmts[0], Expression::StoreLocalVariable { .. }) =>
{
*expr = stmts[0].clone();
simplify_expression(expr)
}
Expand Down
41 changes: 41 additions & 0 deletions internal/compiler/passes/resolving.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ fn resolve_expression(
type_register,
type_loader: Some(type_loader),
current_token: None,
local_variables: vec![],
};

let new_expr = match node.kind() {
Expand Down Expand Up @@ -197,11 +198,15 @@ impl Expression {
fn from_codeblock_node(node: syntax_nodes::CodeBlock, ctx: &mut LookupCtx) -> Expression {
debug_assert_eq!(node.kind(), SyntaxKind::CodeBlock);

// new scope for locals
ctx.local_variables.push(Vec::new());

let mut statements_or_exprs = node
.children()
.filter_map(|n| match n.kind() {
SyntaxKind::Expression => Some(Self::from_expression_node(n.into(), ctx)),
SyntaxKind::ReturnStatement => Some(Self::from_return_statement(n.into(), ctx)),
SyntaxKind::LetStatement => Some(Self::from_let_statement(n.into(), ctx)),
_ => None,
})
.collect::<Vec<_>>();
Expand Down Expand Up @@ -230,9 +235,44 @@ impl Expression {
statements_or_exprs[index] = expr;
});

// pop local scope
ctx.local_variables.pop();

Expression::CodeBlock(statements_or_exprs)
}

fn from_let_statement(node: syntax_nodes::LetStatement, ctx: &mut LookupCtx) -> Expression {
let name = identifier_text(&node.DeclaredIdentifier()).unwrap_or_default();

let global_lookup = crate::lookup::global_lookup();
if let Some(LookupResult::Expression {
expression:
Expression::ReadLocalVariable { .. } | Expression::FunctionParameterReference { .. },
..
}) = global_lookup.lookup(ctx, &name)
{
ctx.diag
.push_error("Redeclaration of local variables is not allowed".to_string(), &node);
return Expression::Invalid;
}

// prefix with "local_" to avoid conflicts
let name: SmolStr = format!("local_{name}",).into();

let value = Self::from_expression_node(node.Expression(), ctx);
let ty = match node.Type() {
Some(ty) => type_from_node(ty, ctx.diag, ctx.type_register),
None => value.ty(),
};

// we can get the last scope exists, because each codeblock creates a new scope and we are inside a codeblock here by necessity
ctx.local_variables.last_mut().unwrap().push((name.clone(), ty.clone()));

let value = Box::new(value.maybe_convert_to(ty.clone(), &node, ctx.diag));

Expression::StoreLocalVariable { name, value }
}

fn from_return_statement(
node: syntax_nodes::ReturnStatement,
ctx: &mut LookupCtx,
Expand Down Expand Up @@ -1639,6 +1679,7 @@ fn resolve_two_way_bindings(
type_register,
type_loader: None,
current_token: Some(node.clone().into()),
local_variables: vec![],
};

binding.expression = Expression::Invalid;
Expand Down
64 changes: 64 additions & 0 deletions internal/compiler/tests/syntax/basic/let.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

export component X inherits Rectangle {
callback aaa();
aaa => {
let foo: string = 42phx;
// ^error{Cannot convert physical-length to string. Divide by 1phx to convert to a plain number}
}

// redeclaration of local variables in same scope
callback bbb();
bbb => {
let foo = "hello";
let foo = "world";
// ^error{Redeclaration of local variables is not allowed}
}

// redeclaration of local variables in same scope with different types
callback ccc();
ccc => {
let foo = "hello";
let foo = 1;
// ^error{Redeclaration of local variables is not allowed}
}

// redeclaration of local variables in different scopes
callback ddd();
ddd => {
let foo = "hello";

if (true) {
let foo = "world";
// ^error{Redeclaration of local variables is not allowed}
}
}

// redeclaration of local variables in different scopes with different types
callback eee();
eee => {
let foo = "hello";

if (root.x > 0) {
let foo = 1;
// ^error{Redeclaration of local variables is not allowed}
}
}

// out of scope access to local variable
callback fff();
fff => {
if (true) {
let bar = "hello";
}

bar;
// ^error{Unknown unqualified identifier 'bar'}
}

callback ggg();
ggg => {
let ggg = 1;
}
}
Loading
Loading