Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 39 additions & 0 deletions docs/php/control-structures.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,45 @@ sidebar:
order: 3
---

## declare

`declare(strict_types=1);` is accepted at the top of a file. elephc compiles a
statically-typed subset and is **always strict**, so the directive is parsed and
treated as a no-op rather than toggling a runtime mode. The `ticks` and `encoding`
directives are likewise accepted and ignored. Directive values must be PHP
literals; `strict_types` must be the first statement, use the statement form, and
have the integer value `0` or `1`.

```php
<?php

declare(strict_types=1);

echo "always strict";
```

The block form runs its body in the enclosing scope:

```php
<?php

declare(ticks=1) {
echo "ok";
}
```

PHP's single-statement and alternative block forms are also accepted:

```php
<?php

declare(ticks=1) echo "single statement";

declare(ticks=1):
echo "alternative syntax";
enddeclare;
```

## if / elseif / else

```php
Expand Down
3 changes: 3 additions & 0 deletions examples/declare-directives/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.s
*.o
main
13 changes: 13 additions & 0 deletions examples/declare-directives/main.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

echo "elephc always uses strict typing\n";

declare(ticks=1) {
echo "braced declare body\n";
}

declare(ticks=1):
echo "alternative declare body\n";
enddeclare;
2 changes: 2 additions & 0 deletions src/lexer/literals/identifiers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ pub(in crate::lexer) fn scan_keyword(cursor: &mut Cursor) -> Result<Token, Compi
"namespace" => Ok(Token::Namespace),
"const" => Ok(Token::Const),
"global" => Ok(Token::Global),
"declare" => Ok(Token::Declare),
"enddeclare" => Ok(Token::EndDeclare),
"static" => Ok(Token::Static),
"self" => Ok(Token::Self_),
"trait" => Ok(Token::Trait),
Expand Down
2 changes: 2 additions & 0 deletions src/lexer/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ pub enum Token {
Namespace, // namespace
Const, // const
Global, // global
Declare, // declare (strict_types/ticks/encoding directive)
EndDeclare, // enddeclare (alternative declare block terminator)
Static, // static
Self_, // self
Trait, // trait
Expand Down
2 changes: 2 additions & 0 deletions src/parser/keyword_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ pub(crate) fn bareword_name_from_token(token: &Token) -> Option<String> {
Token::Namespace => Some("namespace".to_string()),
Token::Const => Some("const".to_string()),
Token::Global => Some("global".to_string()),
Token::Declare => Some("declare".to_string()),
Token::EndDeclare => Some("enddeclare".to_string()),
Token::Static => Some("static".to_string()),
Token::Self_ => Some("self".to_string()),
Token::Trait => Some("trait".to_string()),
Expand Down
189 changes: 189 additions & 0 deletions src/parser/stmt/declare.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//! Purpose:
//! Parses PHP `declare` directives and their statement, braced, or alternative-syntax bodies.
//! Validates PHP's literal-value and `strict_types` placement/form restrictions.
//!
//! Called from:
//! - `crate::parser::stmt::parse_stmt()` when the current token is `declare`.
//!
//! Key details:
//! - Directives are compile-time syntax only because elephc always uses strict typing.
//! - Bodies lower through `Synthetic` so they execute in the enclosing scope.

use crate::errors::CompileError;
use crate::lexer::Token;
use crate::parser::ast::{Stmt, StmtKind};
use crate::span::Span;

use super::{
expect_semicolon, expect_token, parse_block, parse_stmt, recover_to_statement_boundary,
};

/// Parses `declare(directive=literal, ...)` and lowers its effective body to `Synthetic`.
pub(super) fn parse_declare(
tokens: &[(Token, Span)],
pos: &mut usize,
span: Span,
) -> Result<Stmt, CompileError> {
let declare_pos = *pos;
*pos += 1;

expect_token(tokens, pos, &Token::LParen, "Expected '(' after 'declare'")?;
let has_strict_types = parse_directives(tokens, pos, span)?;
expect_token(
tokens,
pos,
&Token::RParen,
"Expected ')' after declare directives",
)?;

if has_strict_types && declare_pos != 1 {
return Err(CompileError::new(
span,
"strict_types declaration must be the very first statement in the script",
));
}

if matches!(
tokens.get(*pos).map(|(token, _)| token),
Some(Token::Semicolon)
) {
*pos += 1;
return Ok(Stmt::new(StmtKind::Synthetic(Vec::new()), span));
}

if has_strict_types {
return Err(CompileError::new(
span,
"strict_types declaration must not use block mode",
));
}

let body = match tokens.get(*pos).map(|(token, _)| token) {
Some(Token::LBrace) => parse_block(tokens, pos)?,
Some(Token::Colon) => parse_alternative_body(tokens, pos)?,
Some(Token::Eof) | None => {
return Err(CompileError::new(
span,
"Expected a statement after declare(...)",
));
}
_ => vec![parse_stmt(tokens, pos)?],
};

Ok(Stmt::new(StmtKind::Synthetic(body), span))
}

/// Parses one or more directive/literal pairs and reports whether `strict_types` occurred.
fn parse_directives(
tokens: &[(Token, Span)],
pos: &mut usize,
declare_span: Span,
) -> Result<bool, CompileError> {
let mut has_strict_types = false;

loop {
let (name, name_span) = match tokens.get(*pos) {
Some((Token::Identifier(name), span)) => (name.clone(), *span),
_ => {
return Err(CompileError::new(
declare_span,
"Expected a directive name in 'declare(...)'",
));
}
};
*pos += 1;

expect_token(
tokens,
pos,
&Token::Assign,
"Expected '=' after declare directive name",
)?;
let integer_value = parse_literal_value(tokens, pos, &name, name_span)?;

if !matches!(
tokens.get(*pos).map(|(token, _)| token),
Some(Token::Comma | Token::RParen)
) {
return Err(CompileError::new(
name_span,
&format!("declare({}) value must be a literal", name),
));
}

if name.eq_ignore_ascii_case("strict_types") {
has_strict_types = true;
if !matches!(integer_value, Some(0 | 1)) {
return Err(CompileError::new(
name_span,
"strict_types declaration must have 0 or 1 as its value",
));
}
}

if !matches!(tokens.get(*pos).map(|(token, _)| token), Some(Token::Comma)) {
break;
}
*pos += 1;
}

Ok(has_strict_types)
}

/// Consumes a PHP declare literal and returns its integer value when it is an integer.
fn parse_literal_value(
tokens: &[(Token, Span)],
pos: &mut usize,
directive: &str,
directive_span: Span,
) -> Result<Option<i64>, CompileError> {
match tokens.get(*pos).map(|(token, _)| token) {
Some(Token::IntLiteral(value)) => {
let value = *value;
*pos += 1;
Ok(Some(value))
}
Some(Token::FloatLiteral(_) | Token::StringLiteral(_)) => {
*pos += 1;
Ok(None)
}
_ => Err(CompileError::new(
directive_span,
&format!("declare({}) value must be a literal", directive),
)),
}
}

/// Parses `: ... enddeclare;`, collecting nested statement errors before closing the block.
fn parse_alternative_body(
tokens: &[(Token, Span)],
pos: &mut usize,
) -> Result<Vec<Stmt>, CompileError> {
*pos += 1;
let mut body = Vec::new();
let mut errors = Vec::new();

while *pos < tokens.len() && !matches!(tokens[*pos].0, Token::EndDeclare | Token::Eof) {
match parse_stmt(tokens, pos) {
Ok(stmt) => body.push(stmt),
Err(error) => {
errors.extend(error.flatten());
recover_to_statement_boundary(tokens, pos);
}
}
}

expect_token(
tokens,
pos,
&Token::EndDeclare,
"Expected 'enddeclare' after declare block",
)?;
expect_semicolon(tokens, pos)?;

if errors.is_empty() {
Ok(body)
} else {
Err(CompileError::from_many(errors))
}
}
6 changes: 6 additions & 0 deletions src/parser/stmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! - Recovery stops at PHP statement boundaries so follow-up diagnostics remain useful.

mod assign;
mod declare;
mod ffi;
mod namespace_use;
mod oop;
Expand Down Expand Up @@ -114,6 +115,7 @@ fn parse_stmt_dispatch(
Token::Function => params::parse_function_decl(tokens, pos, span),
Token::Namespace => namespace_use::parse_namespace_stmt(tokens, pos, span),
Token::Use => namespace_use::parse_use_stmt(tokens, pos, span),
Token::Declare => declare::parse_declare(tokens, pos, span),
Token::Return => simple::parse_return(tokens, pos, span),
Token::Throw => simple::parse_throw(tokens, pos, span),
Token::Yield => {
Expand Down Expand Up @@ -314,6 +316,9 @@ pub(crate) fn recover_to_statement_boundary(tokens: &[(Token, Span)], pos: &mut
Token::RBrace if paren_depth == 0 && bracket_depth == 0 => {
break;
}
Token::EndDeclare if paren_depth == 0 && bracket_depth == 0 => {
break;
}
Token::Eof if paren_depth == 0 && bracket_depth == 0 => {
break;
}
Expand All @@ -334,6 +339,7 @@ pub(crate) fn recover_to_statement_boundary(tokens: &[(Token, Span)], pos: &mut
| Token::Function
| Token::Namespace
| Token::Use
| Token::Declare
| Token::Return
| Token::Throw
| Token::Include
Expand Down
39 changes: 39 additions & 0 deletions tests/codegen/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,45 @@ fn test_top_level_return_value_halts_and_is_discarded() {
assert_eq!(out, "a");
}

/// Verifies `declare(strict_types=1);` is accepted and compiles as a no-op,
/// so the rest of the program runs normally (elephc is always strict).
#[test]
fn test_declare_strict_types_is_accepted_and_noop() {
let out = compile_and_run("<?php declare(strict_types=1); echo 1 + 2;");
assert_eq!(out, "3");
}

/// Verifies the `declare(ticks=1) { ... }` block form runs its body in the
/// enclosing scope.
#[test]
fn test_declare_block_runs_body() {
let out = compile_and_run("<?php declare(ticks=1) { echo \"ok\"; }");
assert_eq!(out, "ok");
}

/// Verifies PHP's alternative declare syntax compiles and executes its body.
#[test]
fn test_declare_alternative_syntax_runs_body() {
let out = compile_and_run("<?php declare(ticks=1): echo \"alternative\"; enddeclare;");
assert_eq!(out, "alternative");
}

/// Verifies PHP's single-statement declare form compiles and executes that statement.
#[test]
fn test_declare_single_statement_runs_body() {
let out = compile_and_run("<?php declare(ticks=1) echo \"single\";");
assert_eq!(out, "single");
}

/// Verifies empty and nested declare bodies preserve normal enclosing-scope execution.
#[test]
fn test_declare_empty_and_nested_bodies() {
let out = compile_and_run(
"<?php declare(ticks=1) {} declare(ticks=1): declare(ticks=1) echo \"nested\"; enddeclare;",
);
assert_eq!(out, "nested");
}

// --- IIFE (Immediately Invoked Function Expression) ---

/// Compiles an IIFE that returns a string literal and verifies the value is echoed correctly.
Expand Down
Loading
Loading