Statements¶
Expression Statement¶
Any expression can be a statement:
The examples in the rest of this page use these declarations:
class User:
name: str = ""
class Config:
debug: bool = False
def load_config() -> Config:
return Config()
def generate_data() -> list[int]:
return [1, 2, 3]
def get_current_time() -> int:
return 0
def compute() -> int:
return 42
def my_function() -> None:
pass
Variable Declaration and Assignment¶
A local variable is declared with let, a constant with const, and bare = assigns:
| Form | Syntax | Meaning |
|---|---|---|
| Inferred declaration | let x = 5 |
New local; type inferred from the initializer |
| Annotated declaration | let x: int = 5 |
New local; type given by the annotation |
| Constant | const NAME[: T] = e |
Assign-once binding (see Constants) |
| Assignment | x = e |
Writes through to the nearest existing binding of x |
let always introduces a fresh, mutable, block-scoped binding. It takes an initializer
(let x: int without one is SPY0104) and a name or a tuple of names as its target
(let a, b = 1, 2, let first, *rest = items). A let of a name that is already bound — in the
same block, an enclosing block, the enclosing function, or the module — is a new variable that
shadows the old one for the rest of its block; it never assigns the old one. let is only allowed
inside a function body (see Module-Level Declaration Rules).
def main() -> None:
let count = 0 # Inferred as int
let name: str = "Alice" # Annotated
let a, b = 1, 2 # Tuple target
let first, *rest = [1, 2, 3] # Starred target
count = count + 1 # Bare '=' writes through to 'count'
print(count, name, a, b, first, rest) # 1 Alice 1 2 1 [2, 3]
Shadowing. Because let is always fresh, it is how a block introduces its own variable
under a name that is already in use; bare = of that name inside the block would instead assign
the outer variable:
def main() -> None:
let x = 5
if x > 0:
let x = 10 # New block-local 'x'; the outer 'x' is untouched
print(x) # 10
print(x) # 5
x = 7 # Bare '=' writes through to the outer 'x'
print(x) # 7
Stage 1 transition (#1974). Sharpy is moving to let-only declarations in stages. In Stage 1,
which is the current language, the keywordless forms still declare: bare x = e on a name that is
not yet bound declares it, and annotated x: T = e declares a new variable (shadowing any existing
one), exactly as before let existed. Stage 2 (#1974) makes let required for a new local and
makes bare = write-through only. The keywordless forms are shown below because Stage 1 code
still uses them:
def main() -> None:
count: int = 0 # Annotated declaration (Stage 1)
name = "Alice" # Bare '=' on a new name declares it (Stage 1)
items = [1, 2, 3] # Inferred as list[int]
count = count + len(items)
print(count, name) # 3 Alice
auto (deprecated; use let). x: auto = e is the Stage 1 spelling of let x = e; auto
is retired in Stage 2 (#1974):
def main() -> None:
count: auto = 0 # Deprecated: write 'let count = 0'
items: auto = [1, 2, 3] # Deprecated: write 'let items = [1, 2, 3]'
print(count, items) # 0 [1, 2, 3]
Module-Level Declaration Rules¶
At module level (outside any function or class), variable declarations have additional constraints.
let Is a Local Declaration¶
let declares a local binding, so it is refused at module level (SPY0340, with the steer "module-scope
bindings are const: use 'const NAME[: T] = ...'"). Module-scope bindings are const:
let is likewise refused in a class, struct or interface body (SPY0340, with the steer "declare a
field as 'x: T = ...' or a constant as 'const X = ...'"), and in a union or enum body it is the
body grammar's parser error.
Type Annotation Required¶
In Stage 1, annotated module-level variables are still accepted; Stage 2 (#1974) makes module scope
const-only. Module-level variables MUST have explicit type annotations:
# ✅ Valid module-level declarations
counter: int = 0
name: str = "default"
items: list[int] = []
data: Config | None = None
# ❌ Invalid - no type annotation at module level
x = 42 # ERROR: requires type annotation
name = "hello" # ERROR: requires type annotation
Rationale: This rule eliminates ambiguity between static field declarations and executable statements, which would otherwise look identical syntactically.
No Executable Statements¶
Bare expression statements are not allowed at module level:
# ❌ NOT allowed at module level
print("hello") # ERROR: executable statement not allowed
my_function() # ERROR: executable statement not allowed
obj.method() # ERROR: executable statement not allowed
1 + 2 # ERROR: executable statement not allowed
# ✅ Move into main() or another function
def main():
print("hello") # OK inside function
my_function() # OK inside function
Function Calls in Initializers¶
Function calls ARE allowed as part of a variable initializer:
# ✅ Valid - function call is part of initialization
config: Config = load_config()
data: list[int] = generate_data()
timestamp: int = get_current_time()
Inside Functions¶
Inside functions (including main()), let declares with type inference (and, in Stage 1, so
does bare = on a new name):
def main():
let x = 42 # ✅ OK - inferred as int
let result = compute() # ✅ OK - inferred from return type
name = "hello" # ✅ OK in Stage 1 - inferred as str
print(x, result, name) # 42 42 hello
Bare Declarations¶
Sharpy allows variable declarations without initialization (bare declarations). The variable must be assigned on all control-flow paths before it is read — use-before-assign is a compile-time error (SPY0600). See variable_declaration.md for details.
def choose(condition: bool) -> None:
x: int
if condition:
x = 1
else:
x = 2
print(x) # OK — assigned on all paths
Class and struct fields can also be declared without initialization if they are assigned in __init__:
class Person:
# Field declarations (no initializer required)
name: str
age: int
# Optional: fields with default values
active: bool = True
def __init__(self, name: str, age: int):
# All fields without defaults must be assigned in __init__
self.name = name
self.age = age
Declaration Keywords¶
Sharpy has two declaration keywords: let for a fresh, mutable, block-scoped local and const for
an assign-once binding. There is no mutability axis — var, val and let mut are not Sharpy
syntax:
def main() -> None:
let x = 5 # Fresh binding, type inferred
let y: int = 10 # Fresh binding, type explicit
const Z = 15 # Assign-once
x = x + y + Z # Bare '=' writes through to 'x'
print(x) # 30
def main() -> None:
var y = 10 # ERROR (SPY0103): 'var' is not a keyword
val z = 15 # ERROR (SPY0103): 'val' is not a keyword
Constants¶
Constants are declared with const and are assign-once. The initializer may be any expression: a
compile-time constant lowers to C# const, anything else to static readonly:
# Module-level constants
const PI: float = 3.14159
const MAX_SIZE: int = 1000
const APP_NAME = "MyApp" # Type inferred as str
const DEBUG: bool = True
const ANSWER: int = compute() # Runtime initializer: emits 'static readonly'
def main() -> None:
print(PI, MAX_SIZE, APP_NAME, DEBUG, ANSWER) # 3.14159 1000 MyApp True 42
Class-Level Constants:
Constants can also be declared within classes. Class-level constants are implicitly @static (matching C# semantics where class constants are always static):
class Math:
const PI: float = 3.14159265358979
const E: float = 2.71828182845904
const TAU: float = 6.28318530717958
@static
def circle_area(radius: float) -> float:
return Math.PI * radius ** 2
class HttpStatus:
const OK: int = 200
const NOT_FOUND: int = 404
const INTERNAL_ERROR: int = 500
def main() -> None:
# Access via class name (constants are implicitly static)
print(Math.PI) # 3.14159265358979
print(HttpStatus.OK) # 200
# Cannot access via instance (they're static, not per-instance)
m = Math()
print(m.PI) # Works but discouraged; prefer Math.PI
Note: There is no such thing as a per-instance constant. Use a read-only property (property get) with a backing field or @final field (if added in a future version) for per-instance immutability.
Constants cannot be reassigned:
Implementation
- ✅ Native - Direct mapping to C# variable declarations and const. let x = e emits the same C#
local declaration as a Stage 1 keywordless declaration (var x = e, or the annotated type); a
let that shadows is versioned like an annotated shadow (x_1).