diff --git a/docs/php/control-structures.md b/docs/php/control-structures.md index 1c920b420f..19815ec934 100644 --- a/docs/php/control-structures.md +++ b/docs/php/control-structures.md @@ -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 + Result 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), diff --git a/src/lexer/token.rs b/src/lexer/token.rs index a611c5ce79..6b651724b9 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -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 diff --git a/src/parser/keyword_name.rs b/src/parser/keyword_name.rs index 0d871c9760..5d95d0351f 100644 --- a/src/parser/keyword_name.rs +++ b/src/parser/keyword_name.rs @@ -64,6 +64,8 @@ pub(crate) fn bareword_name_from_token(token: &Token) -> Option { 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()), diff --git a/src/parser/stmt/declare.rs b/src/parser/stmt/declare.rs new file mode 100644 index 0000000000..c156787377 --- /dev/null +++ b/src/parser/stmt/declare.rs @@ -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 { + 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 { + 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, 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, 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)) + } +} diff --git a/src/parser/stmt/mod.rs b/src/parser/stmt/mod.rs index cdb61be423..6befd9cda1 100644 --- a/src/parser/stmt/mod.rs +++ b/src/parser/stmt/mod.rs @@ -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; @@ -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 => { @@ -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; } @@ -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 diff --git a/tests/codegen/misc.rs b/tests/codegen/misc.rs index 18dd3937f8..4f751134c8 100644 --- a/tests/codegen/misc.rs +++ b/tests/codegen/misc.rs @@ -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(" assert!(body.is_empty()), + other => panic!("Expected empty Synthetic, got {:?}", other), + } +} + +/// Verifies `declare(ticks=1) { ... }` (block form) parses to a `Synthetic` +/// wrapper around its body statements. +#[test] +fn test_declare_block_parses_to_synthetic_body() { + let stmts = parse_source(" assert_eq!(body.len(), 1), + other => panic!("Expected Synthetic body, got {:?}", other), + } +} + +/// Verifies PHP's alternative `declare: ... enddeclare;` syntax preserves its body. +#[test] +fn test_declare_alternative_syntax_parses_to_synthetic_body() { + let stmts = parse_source(" assert_eq!(body.len(), 1), + other => panic!("Expected Synthetic body, got {:?}", other), + } +} + +/// Verifies PHP's single-statement `declare(...) statement` form wraps that statement. +#[test] +fn test_declare_single_statement_parses_to_synthetic_body() { + let stmts = parse_source(" assert_eq!(body.len(), 1), + other => panic!("Expected Synthetic body, got {:?}", other), + } +} + +/// Verifies declare values accept PHP literal float and string forms. +#[test] +fn test_declare_accepts_php_literal_value_kinds() { + let stmts = parse_source("