machine-dialect 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- machine_dialect/__main__.py +667 -0
- machine_dialect/agent/__init__.py +5 -0
- machine_dialect/agent/agent.py +360 -0
- machine_dialect/ast/__init__.py +95 -0
- machine_dialect/ast/ast_node.py +35 -0
- machine_dialect/ast/call_expression.py +82 -0
- machine_dialect/ast/dict_extraction.py +60 -0
- machine_dialect/ast/expressions.py +439 -0
- machine_dialect/ast/literals.py +309 -0
- machine_dialect/ast/program.py +35 -0
- machine_dialect/ast/statements.py +1433 -0
- machine_dialect/ast/tests/test_ast_string_representation.py +62 -0
- machine_dialect/ast/tests/test_boolean_literal.py +29 -0
- machine_dialect/ast/tests/test_collection_hir.py +138 -0
- machine_dialect/ast/tests/test_define_statement.py +142 -0
- machine_dialect/ast/tests/test_desugar.py +541 -0
- machine_dialect/ast/tests/test_foreach_desugar.py +245 -0
- machine_dialect/cfg/__init__.py +6 -0
- machine_dialect/cfg/config.py +156 -0
- machine_dialect/cfg/examples.py +221 -0
- machine_dialect/cfg/generate_with_ai.py +187 -0
- machine_dialect/cfg/openai_generation.py +200 -0
- machine_dialect/cfg/parser.py +94 -0
- machine_dialect/cfg/tests/__init__.py +1 -0
- machine_dialect/cfg/tests/test_cfg_parser.py +252 -0
- machine_dialect/cfg/tests/test_config.py +188 -0
- machine_dialect/cfg/tests/test_examples.py +391 -0
- machine_dialect/cfg/tests/test_generate_with_ai.py +354 -0
- machine_dialect/cfg/tests/test_openai_generation.py +256 -0
- machine_dialect/codegen/__init__.py +5 -0
- machine_dialect/codegen/bytecode_module.py +89 -0
- machine_dialect/codegen/bytecode_serializer.py +300 -0
- machine_dialect/codegen/opcodes.py +101 -0
- machine_dialect/codegen/register_codegen.py +1996 -0
- machine_dialect/codegen/symtab.py +208 -0
- machine_dialect/codegen/tests/__init__.py +1 -0
- machine_dialect/codegen/tests/test_array_operations_codegen.py +295 -0
- machine_dialect/codegen/tests/test_bytecode_serializer.py +185 -0
- machine_dialect/codegen/tests/test_register_codegen_ssa.py +324 -0
- machine_dialect/codegen/tests/test_symtab.py +418 -0
- machine_dialect/codegen/vm_serializer.py +621 -0
- machine_dialect/compiler/__init__.py +18 -0
- machine_dialect/compiler/compiler.py +197 -0
- machine_dialect/compiler/config.py +149 -0
- machine_dialect/compiler/context.py +149 -0
- machine_dialect/compiler/phases/__init__.py +19 -0
- machine_dialect/compiler/phases/bytecode_optimization.py +90 -0
- machine_dialect/compiler/phases/codegen.py +40 -0
- machine_dialect/compiler/phases/hir_generation.py +39 -0
- machine_dialect/compiler/phases/mir_generation.py +86 -0
- machine_dialect/compiler/phases/optimization.py +110 -0
- machine_dialect/compiler/phases/parsing.py +39 -0
- machine_dialect/compiler/pipeline.py +143 -0
- machine_dialect/compiler/tests/__init__.py +1 -0
- machine_dialect/compiler/tests/test_compiler.py +568 -0
- machine_dialect/compiler/vm_runner.py +173 -0
- machine_dialect/errors/__init__.py +32 -0
- machine_dialect/errors/exceptions.py +369 -0
- machine_dialect/errors/messages.py +82 -0
- machine_dialect/errors/tests/__init__.py +0 -0
- machine_dialect/errors/tests/test_expected_token_errors.py +188 -0
- machine_dialect/errors/tests/test_name_errors.py +118 -0
- machine_dialect/helpers/__init__.py +0 -0
- machine_dialect/helpers/stopwords.py +225 -0
- machine_dialect/helpers/validators.py +30 -0
- machine_dialect/lexer/__init__.py +9 -0
- machine_dialect/lexer/constants.py +23 -0
- machine_dialect/lexer/lexer.py +907 -0
- machine_dialect/lexer/tests/__init__.py +0 -0
- machine_dialect/lexer/tests/helpers.py +86 -0
- machine_dialect/lexer/tests/test_apostrophe_identifiers.py +122 -0
- machine_dialect/lexer/tests/test_backtick_identifiers.py +140 -0
- machine_dialect/lexer/tests/test_boolean_literals.py +108 -0
- machine_dialect/lexer/tests/test_case_insensitive_keywords.py +188 -0
- machine_dialect/lexer/tests/test_comments.py +200 -0
- machine_dialect/lexer/tests/test_double_asterisk_keywords.py +127 -0
- machine_dialect/lexer/tests/test_lexer_position.py +113 -0
- machine_dialect/lexer/tests/test_list_tokens.py +282 -0
- machine_dialect/lexer/tests/test_stopwords.py +80 -0
- machine_dialect/lexer/tests/test_strict_equality.py +129 -0
- machine_dialect/lexer/tests/test_token.py +41 -0
- machine_dialect/lexer/tests/test_tokenization.py +294 -0
- machine_dialect/lexer/tests/test_underscore_literals.py +343 -0
- machine_dialect/lexer/tests/test_url_literals.py +169 -0
- machine_dialect/lexer/tokens.py +487 -0
- machine_dialect/linter/__init__.py +10 -0
- machine_dialect/linter/__main__.py +144 -0
- machine_dialect/linter/linter.py +154 -0
- machine_dialect/linter/rules/__init__.py +8 -0
- machine_dialect/linter/rules/base.py +112 -0
- machine_dialect/linter/rules/statement_termination.py +99 -0
- machine_dialect/linter/tests/__init__.py +1 -0
- machine_dialect/linter/tests/mdrules/__init__.py +0 -0
- machine_dialect/linter/tests/mdrules/test_md101_statement_termination.py +181 -0
- machine_dialect/linter/tests/test_linter.py +81 -0
- machine_dialect/linter/tests/test_rules.py +110 -0
- machine_dialect/linter/tests/test_violations.py +71 -0
- machine_dialect/linter/violations.py +51 -0
- machine_dialect/mir/__init__.py +69 -0
- machine_dialect/mir/analyses/__init__.py +20 -0
- machine_dialect/mir/analyses/alias_analysis.py +315 -0
- machine_dialect/mir/analyses/dominance_analysis.py +49 -0
- machine_dialect/mir/analyses/escape_analysis.py +286 -0
- machine_dialect/mir/analyses/loop_analysis.py +272 -0
- machine_dialect/mir/analyses/tests/test_type_analysis.py +736 -0
- machine_dialect/mir/analyses/type_analysis.py +448 -0
- machine_dialect/mir/analyses/use_def_chains.py +232 -0
- machine_dialect/mir/basic_block.py +385 -0
- machine_dialect/mir/dataflow.py +445 -0
- machine_dialect/mir/debug_info.py +208 -0
- machine_dialect/mir/hir_to_mir.py +1738 -0
- machine_dialect/mir/mir_dumper.py +366 -0
- machine_dialect/mir/mir_function.py +167 -0
- machine_dialect/mir/mir_instructions.py +1877 -0
- machine_dialect/mir/mir_interpreter.py +556 -0
- machine_dialect/mir/mir_module.py +225 -0
- machine_dialect/mir/mir_printer.py +480 -0
- machine_dialect/mir/mir_transformer.py +410 -0
- machine_dialect/mir/mir_types.py +367 -0
- machine_dialect/mir/mir_validation.py +455 -0
- machine_dialect/mir/mir_values.py +268 -0
- machine_dialect/mir/optimization_config.py +233 -0
- machine_dialect/mir/optimization_pass.py +251 -0
- machine_dialect/mir/optimization_pipeline.py +355 -0
- machine_dialect/mir/optimizations/__init__.py +84 -0
- machine_dialect/mir/optimizations/algebraic_simplification.py +733 -0
- machine_dialect/mir/optimizations/branch_prediction.py +372 -0
- machine_dialect/mir/optimizations/constant_propagation.py +634 -0
- machine_dialect/mir/optimizations/cse.py +398 -0
- machine_dialect/mir/optimizations/dce.py +288 -0
- machine_dialect/mir/optimizations/inlining.py +551 -0
- machine_dialect/mir/optimizations/jump_threading.py +487 -0
- machine_dialect/mir/optimizations/licm.py +405 -0
- machine_dialect/mir/optimizations/loop_unrolling.py +366 -0
- machine_dialect/mir/optimizations/strength_reduction.py +422 -0
- machine_dialect/mir/optimizations/tail_call.py +207 -0
- machine_dialect/mir/optimizations/tests/test_loop_unrolling.py +483 -0
- machine_dialect/mir/optimizations/type_narrowing.py +397 -0
- machine_dialect/mir/optimizations/type_specialization.py +447 -0
- machine_dialect/mir/optimizations/type_specific.py +906 -0
- machine_dialect/mir/optimize_mir.py +89 -0
- machine_dialect/mir/pass_manager.py +391 -0
- machine_dialect/mir/profiling/__init__.py +26 -0
- machine_dialect/mir/profiling/profile_collector.py +318 -0
- machine_dialect/mir/profiling/profile_data.py +372 -0
- machine_dialect/mir/profiling/profile_reader.py +272 -0
- machine_dialect/mir/profiling/profile_writer.py +226 -0
- machine_dialect/mir/register_allocation.py +302 -0
- machine_dialect/mir/reporting/__init__.py +17 -0
- machine_dialect/mir/reporting/optimization_reporter.py +314 -0
- machine_dialect/mir/reporting/report_formatter.py +289 -0
- machine_dialect/mir/ssa_construction.py +342 -0
- machine_dialect/mir/tests/__init__.py +1 -0
- machine_dialect/mir/tests/test_algebraic_associativity.py +204 -0
- machine_dialect/mir/tests/test_algebraic_complex_patterns.py +221 -0
- machine_dialect/mir/tests/test_algebraic_division.py +126 -0
- machine_dialect/mir/tests/test_algebraic_simplification.py +863 -0
- machine_dialect/mir/tests/test_basic_block.py +425 -0
- machine_dialect/mir/tests/test_branch_prediction.py +459 -0
- machine_dialect/mir/tests/test_call_lowering.py +168 -0
- machine_dialect/mir/tests/test_collection_lowering.py +604 -0
- machine_dialect/mir/tests/test_cross_block_constant_propagation.py +255 -0
- machine_dialect/mir/tests/test_custom_passes.py +166 -0
- machine_dialect/mir/tests/test_debug_info.py +285 -0
- machine_dialect/mir/tests/test_dict_extraction_lowering.py +192 -0
- machine_dialect/mir/tests/test_dictionary_lowering.py +299 -0
- machine_dialect/mir/tests/test_double_negation.py +231 -0
- machine_dialect/mir/tests/test_escape_analysis.py +233 -0
- machine_dialect/mir/tests/test_hir_to_mir.py +465 -0
- machine_dialect/mir/tests/test_hir_to_mir_complete.py +389 -0
- machine_dialect/mir/tests/test_hir_to_mir_simple.py +130 -0
- machine_dialect/mir/tests/test_inlining.py +435 -0
- machine_dialect/mir/tests/test_licm.py +472 -0
- machine_dialect/mir/tests/test_mir_dumper.py +313 -0
- machine_dialect/mir/tests/test_mir_instructions.py +445 -0
- machine_dialect/mir/tests/test_mir_module.py +860 -0
- machine_dialect/mir/tests/test_mir_printer.py +387 -0
- machine_dialect/mir/tests/test_mir_types.py +123 -0
- machine_dialect/mir/tests/test_mir_types_enhanced.py +132 -0
- machine_dialect/mir/tests/test_mir_validation.py +378 -0
- machine_dialect/mir/tests/test_mir_values.py +168 -0
- machine_dialect/mir/tests/test_one_based_indexing.py +202 -0
- machine_dialect/mir/tests/test_optimization_helpers.py +60 -0
- machine_dialect/mir/tests/test_optimization_pipeline.py +554 -0
- machine_dialect/mir/tests/test_optimization_reporter.py +318 -0
- machine_dialect/mir/tests/test_pass_manager.py +294 -0
- machine_dialect/mir/tests/test_pass_registration.py +64 -0
- machine_dialect/mir/tests/test_profiling.py +356 -0
- machine_dialect/mir/tests/test_register_allocation.py +307 -0
- machine_dialect/mir/tests/test_report_formatters.py +372 -0
- machine_dialect/mir/tests/test_ssa_construction.py +433 -0
- machine_dialect/mir/tests/test_tail_call.py +236 -0
- machine_dialect/mir/tests/test_type_annotated_instructions.py +192 -0
- machine_dialect/mir/tests/test_type_narrowing.py +277 -0
- machine_dialect/mir/tests/test_type_specialization.py +421 -0
- machine_dialect/mir/tests/test_type_specific_optimization.py +545 -0
- machine_dialect/mir/tests/test_type_specific_optimization_advanced.py +382 -0
- machine_dialect/mir/type_inference.py +368 -0
- machine_dialect/parser/__init__.py +12 -0
- machine_dialect/parser/enums.py +45 -0
- machine_dialect/parser/parser.py +3655 -0
- machine_dialect/parser/protocols.py +11 -0
- machine_dialect/parser/symbol_table.py +169 -0
- machine_dialect/parser/tests/__init__.py +0 -0
- machine_dialect/parser/tests/helper_functions.py +193 -0
- machine_dialect/parser/tests/test_action_statements.py +334 -0
- machine_dialect/parser/tests/test_boolean_literal_expressions.py +152 -0
- machine_dialect/parser/tests/test_call_statements.py +154 -0
- machine_dialect/parser/tests/test_call_statements_errors.py +187 -0
- machine_dialect/parser/tests/test_collection_mutations.py +264 -0
- machine_dialect/parser/tests/test_conditional_expressions.py +343 -0
- machine_dialect/parser/tests/test_define_integration.py +468 -0
- machine_dialect/parser/tests/test_define_statements.py +311 -0
- machine_dialect/parser/tests/test_dict_extraction.py +115 -0
- machine_dialect/parser/tests/test_empty_literal.py +155 -0
- machine_dialect/parser/tests/test_float_literal_expressions.py +163 -0
- machine_dialect/parser/tests/test_identifier_expressions.py +57 -0
- machine_dialect/parser/tests/test_if_empty_block.py +61 -0
- machine_dialect/parser/tests/test_if_statements.py +299 -0
- machine_dialect/parser/tests/test_illegal_tokens.py +86 -0
- machine_dialect/parser/tests/test_infix_expressions.py +680 -0
- machine_dialect/parser/tests/test_integer_literal_expressions.py +137 -0
- machine_dialect/parser/tests/test_interaction_statements.py +269 -0
- machine_dialect/parser/tests/test_list_literals.py +277 -0
- machine_dialect/parser/tests/test_no_none_in_ast.py +94 -0
- machine_dialect/parser/tests/test_panic_mode_recovery.py +171 -0
- machine_dialect/parser/tests/test_parse_errors.py +114 -0
- machine_dialect/parser/tests/test_possessive_syntax.py +182 -0
- machine_dialect/parser/tests/test_prefix_expressions.py +415 -0
- machine_dialect/parser/tests/test_program.py +13 -0
- machine_dialect/parser/tests/test_return_statements.py +89 -0
- machine_dialect/parser/tests/test_set_statements.py +152 -0
- machine_dialect/parser/tests/test_strict_equality.py +258 -0
- machine_dialect/parser/tests/test_symbol_table.py +217 -0
- machine_dialect/parser/tests/test_url_literal_expressions.py +209 -0
- machine_dialect/parser/tests/test_utility_statements.py +423 -0
- machine_dialect/parser/token_buffer.py +159 -0
- machine_dialect/repl/__init__.py +3 -0
- machine_dialect/repl/repl.py +426 -0
- machine_dialect/repl/tests/__init__.py +0 -0
- machine_dialect/repl/tests/test_repl.py +606 -0
- machine_dialect/semantic/__init__.py +12 -0
- machine_dialect/semantic/analyzer.py +906 -0
- machine_dialect/semantic/error_messages.py +189 -0
- machine_dialect/semantic/tests/__init__.py +1 -0
- machine_dialect/semantic/tests/test_analyzer.py +364 -0
- machine_dialect/semantic/tests/test_error_messages.py +104 -0
- machine_dialect/tests/edge_cases/__init__.py +10 -0
- machine_dialect/tests/edge_cases/test_boundary_access.py +256 -0
- machine_dialect/tests/edge_cases/test_empty_collections.py +166 -0
- machine_dialect/tests/edge_cases/test_invalid_operations.py +243 -0
- machine_dialect/tests/edge_cases/test_named_list_edge_cases.py +295 -0
- machine_dialect/tests/edge_cases/test_nested_structures.py +313 -0
- machine_dialect/tests/edge_cases/test_type_mixing.py +277 -0
- machine_dialect/tests/integration/test_array_operations_emulation.py +248 -0
- machine_dialect/tests/integration/test_list_compilation.py +395 -0
- machine_dialect/tests/integration/test_lists_and_dictionaries.py +322 -0
- machine_dialect/type_checking/__init__.py +21 -0
- machine_dialect/type_checking/tests/__init__.py +1 -0
- machine_dialect/type_checking/tests/test_type_system.py +230 -0
- machine_dialect/type_checking/type_system.py +270 -0
- machine_dialect-0.1.0a1.dist-info/METADATA +128 -0
- machine_dialect-0.1.0a1.dist-info/RECORD +268 -0
- machine_dialect-0.1.0a1.dist-info/WHEEL +5 -0
- machine_dialect-0.1.0a1.dist-info/entry_points.txt +3 -0
- machine_dialect-0.1.0a1.dist-info/licenses/LICENSE +201 -0
- machine_dialect-0.1.0a1.dist-info/top_level.txt +2 -0
- machine_dialect_vm/__init__.pyi +15 -0
@@ -0,0 +1,258 @@
|
|
1
|
+
"""Tests for parsing strict equality expressions.
|
2
|
+
|
3
|
+
This module tests the parser's ability to handle strict equality and
|
4
|
+
strict inequality expressions, ensuring they are distinguished from
|
5
|
+
regular equality operators.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import pytest
|
9
|
+
|
10
|
+
from machine_dialect.ast import ExpressionStatement, InfixExpression
|
11
|
+
from machine_dialect.parser import Parser
|
12
|
+
from machine_dialect.parser.tests.helper_functions import (
|
13
|
+
assert_infix_expression,
|
14
|
+
assert_program_statements,
|
15
|
+
)
|
16
|
+
|
17
|
+
|
18
|
+
class TestStrictEqualityExpressions:
|
19
|
+
"""Test parsing of strict equality expressions."""
|
20
|
+
|
21
|
+
@pytest.mark.parametrize(
|
22
|
+
"source,left,operator,right",
|
23
|
+
[
|
24
|
+
# Strict equality with integers
|
25
|
+
("5 is strictly equal to 5", 5, "is strictly equal to", 5),
|
26
|
+
("10 is exactly equal to 10", 10, "is strictly equal to", 10),
|
27
|
+
("42 is identical to 42", 42, "is strictly equal to", 42),
|
28
|
+
# Strict inequality with integers
|
29
|
+
("5 is not strictly equal to 10", 5, "is not strictly equal to", 10),
|
30
|
+
("10 is not exactly equal to 20", 10, "is not strictly equal to", 20),
|
31
|
+
("7 is not identical to 8", 7, "is not strictly equal to", 8),
|
32
|
+
# Strict equality with floats
|
33
|
+
("3.14 is strictly equal to 3.14", 3.14, "is strictly equal to", 3.14),
|
34
|
+
("2.5 is exactly equal to 2.5", 2.5, "is strictly equal to", 2.5),
|
35
|
+
# Strict equality with booleans
|
36
|
+
("Yes is strictly equal to Yes", True, "is strictly equal to", True),
|
37
|
+
("No is identical to No", False, "is strictly equal to", False),
|
38
|
+
# Strict equality with identifiers
|
39
|
+
("x is strictly equal to y", "x", "is strictly equal to", "y"),
|
40
|
+
("foo is exactly equal to bar", "foo", "is strictly equal to", "bar"),
|
41
|
+
("`value` is identical to expected", "value", "is strictly equal to", "expected"),
|
42
|
+
# Mixed types (would fail at runtime for strict equality)
|
43
|
+
("5 is strictly equal to 5.0", 5, "is strictly equal to", 5.0),
|
44
|
+
("Yes is strictly equal to 1", True, "is strictly equal to", 1),
|
45
|
+
],
|
46
|
+
)
|
47
|
+
def test_strict_equality_expressions(
|
48
|
+
self, source: str, left: int | float | bool | str, operator: str, right: int | float | bool | str
|
49
|
+
) -> None:
|
50
|
+
"""Test parsing strict equality and inequality expressions.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
source: The source code containing a strict equality expression.
|
54
|
+
left: Expected left operand value.
|
55
|
+
operator: Expected operator string representation.
|
56
|
+
right: Expected right operand value.
|
57
|
+
"""
|
58
|
+
parser = Parser()
|
59
|
+
program = parser.parse(source, check_semantics=False)
|
60
|
+
|
61
|
+
assert len(parser.errors) == 0, f"Parser errors: {parser.errors}"
|
62
|
+
assert_program_statements(parser, program)
|
63
|
+
|
64
|
+
statement = program.statements[0]
|
65
|
+
assert isinstance(statement, ExpressionStatement)
|
66
|
+
assert statement.expression is not None
|
67
|
+
|
68
|
+
assert_infix_expression(statement.expression, left, operator, right)
|
69
|
+
|
70
|
+
def test_strict_vs_value_equality(self) -> None:
|
71
|
+
"""Test that strict and value equality are parsed as different operators."""
|
72
|
+
# Value equality
|
73
|
+
parser1 = Parser()
|
74
|
+
program1 = parser1.parse("x equals y")
|
75
|
+
assert len(parser1.errors) == 0
|
76
|
+
|
77
|
+
statement1 = program1.statements[0]
|
78
|
+
assert isinstance(statement1, ExpressionStatement)
|
79
|
+
expr1 = statement1.expression
|
80
|
+
assert isinstance(expr1, InfixExpression)
|
81
|
+
assert expr1.operator == "equals"
|
82
|
+
|
83
|
+
# Strict equality
|
84
|
+
parser2 = Parser()
|
85
|
+
program2 = parser2.parse("x is strictly equal to y")
|
86
|
+
assert len(parser2.errors) == 0
|
87
|
+
|
88
|
+
statement2 = program2.statements[0]
|
89
|
+
assert isinstance(statement2, ExpressionStatement)
|
90
|
+
expr2 = statement2.expression
|
91
|
+
assert isinstance(expr2, InfixExpression)
|
92
|
+
assert expr2.operator == "is strictly equal to"
|
93
|
+
|
94
|
+
# Ensure they're different
|
95
|
+
assert expr1.operator != expr2.operator
|
96
|
+
|
97
|
+
def test_strict_equality_precedence(self) -> None:
|
98
|
+
"""Test that strict equality has the same precedence as regular equality."""
|
99
|
+
test_cases = [
|
100
|
+
# Arithmetic has higher precedence than strict equality
|
101
|
+
("5 + 3 is strictly equal to 8", "((_5_ + _3_) is strictly equal to _8_)"),
|
102
|
+
("2 * 3 is exactly equal to 6", "((_2_ * _3_) is strictly equal to _6_)"),
|
103
|
+
# Logical operators have lower precedence
|
104
|
+
(
|
105
|
+
"x is strictly equal to 5 and y is strictly equal to 10",
|
106
|
+
"((`x` is strictly equal to _5_) and (`y` is strictly equal to _10_))",
|
107
|
+
),
|
108
|
+
(
|
109
|
+
"`a` is identical to `b` or `c` is not identical to `d`",
|
110
|
+
"((`a` is strictly equal to `b`) or (`c` is not strictly equal to `d`))",
|
111
|
+
),
|
112
|
+
# Mixed with regular equality
|
113
|
+
(
|
114
|
+
"x equals y and z is strictly equal to w",
|
115
|
+
"((`x` equals `y`) and (`z` is strictly equal to `w`))",
|
116
|
+
),
|
117
|
+
# Strict inequality with precedence
|
118
|
+
(
|
119
|
+
"5 + 3 is not strictly equal to 10",
|
120
|
+
"((_5_ + _3_) is not strictly equal to _10_)",
|
121
|
+
),
|
122
|
+
(
|
123
|
+
"x * 2 is not exactly equal to y",
|
124
|
+
"((`x` * _2_) is not strictly equal to `y`)",
|
125
|
+
),
|
126
|
+
]
|
127
|
+
|
128
|
+
for source, expected_structure in test_cases:
|
129
|
+
parser = Parser()
|
130
|
+
program = parser.parse(source, check_semantics=False)
|
131
|
+
|
132
|
+
assert len(parser.errors) == 0, f"Parser errors for '{source}': {parser.errors}"
|
133
|
+
assert len(program.statements) == 1
|
134
|
+
|
135
|
+
statement = program.statements[0]
|
136
|
+
assert isinstance(statement, ExpressionStatement)
|
137
|
+
assert statement.expression is not None
|
138
|
+
|
139
|
+
# Check string representation matches expected precedence
|
140
|
+
actual = str(statement.expression)
|
141
|
+
assert actual == expected_structure, f"For '{source}': expected {expected_structure}, got {actual}"
|
142
|
+
|
143
|
+
def test_strict_equality_in_conditionals(self) -> None:
|
144
|
+
"""Test strict equality in if statements."""
|
145
|
+
from machine_dialect.ast import BlockStatement, IfStatement
|
146
|
+
|
147
|
+
test_cases = [
|
148
|
+
# Basic if with strict equality
|
149
|
+
(
|
150
|
+
"""
|
151
|
+
if x is strictly equal to 5 then:
|
152
|
+
> give back _yes_.
|
153
|
+
""",
|
154
|
+
"x",
|
155
|
+
"is strictly equal to",
|
156
|
+
5,
|
157
|
+
),
|
158
|
+
# If with strict inequality
|
159
|
+
(
|
160
|
+
"""
|
161
|
+
Define `result` as Text or Empty.
|
162
|
+
if `value` is not strictly equal to empty then:
|
163
|
+
> set `result` to `value`.
|
164
|
+
""",
|
165
|
+
"value",
|
166
|
+
"is not strictly equal to",
|
167
|
+
"empty",
|
168
|
+
),
|
169
|
+
# If-else with strict equality
|
170
|
+
(
|
171
|
+
"""
|
172
|
+
if `a` is exactly equal to `b` then:
|
173
|
+
> give back _Same_.
|
174
|
+
else:
|
175
|
+
> give back _Different_.
|
176
|
+
""",
|
177
|
+
"a",
|
178
|
+
"is strictly equal to",
|
179
|
+
"b",
|
180
|
+
),
|
181
|
+
# Complex condition with strict equality
|
182
|
+
(
|
183
|
+
"""
|
184
|
+
Define `flag` as Yes/No.
|
185
|
+
if x is identical to 0 or y is not identical to 0 then:
|
186
|
+
> set `flag` to _yes_.
|
187
|
+
""",
|
188
|
+
None, # Complex condition, skip simple checks
|
189
|
+
None,
|
190
|
+
None,
|
191
|
+
),
|
192
|
+
]
|
193
|
+
|
194
|
+
for test_input in test_cases:
|
195
|
+
source = test_input[0]
|
196
|
+
expected_left = test_input[1]
|
197
|
+
expected_op = test_input[2]
|
198
|
+
expected_right = test_input[3]
|
199
|
+
|
200
|
+
parser = Parser()
|
201
|
+
program = parser.parse(source, check_semantics=False)
|
202
|
+
|
203
|
+
assert len(parser.errors) == 0, f"Parser errors: {parser.errors}"
|
204
|
+
|
205
|
+
# Skip Define statement if present
|
206
|
+
stmt_idx = 0
|
207
|
+
if "Define" in source:
|
208
|
+
stmt_idx = 1
|
209
|
+
assert len(program.statements) == 2
|
210
|
+
else:
|
211
|
+
assert len(program.statements) == 1
|
212
|
+
|
213
|
+
# Check it's an if statement
|
214
|
+
if_stmt = program.statements[stmt_idx]
|
215
|
+
assert isinstance(if_stmt, IfStatement)
|
216
|
+
|
217
|
+
# Check the condition is parsed correctly
|
218
|
+
if expected_left is not None: # Simple condition check
|
219
|
+
condition = if_stmt.condition
|
220
|
+
assert isinstance(condition, InfixExpression)
|
221
|
+
assert condition.operator == expected_op
|
222
|
+
|
223
|
+
# Check left operand
|
224
|
+
if isinstance(expected_left, str):
|
225
|
+
assert str(condition.left) == f"`{expected_left}`"
|
226
|
+
else:
|
227
|
+
assert str(condition.left) == f"_{expected_left}_"
|
228
|
+
|
229
|
+
# Check right operand
|
230
|
+
if isinstance(expected_right, str):
|
231
|
+
# Special keywords like empty are parsed differently
|
232
|
+
if expected_right == "empty":
|
233
|
+
assert str(condition.right) == "empty"
|
234
|
+
else:
|
235
|
+
assert str(condition.right) == f"`{expected_right}`"
|
236
|
+
else:
|
237
|
+
assert str(condition.right) == f"_{expected_right}_"
|
238
|
+
|
239
|
+
# Check consequence block exists
|
240
|
+
assert isinstance(if_stmt.consequence, BlockStatement)
|
241
|
+
assert len(if_stmt.consequence.statements) > 0
|
242
|
+
|
243
|
+
def test_complex_expressions_with_strict_equality(self) -> None:
|
244
|
+
"""Test complex expressions involving strict equality."""
|
245
|
+
test_cases = [
|
246
|
+
"not x is strictly equal to y",
|
247
|
+
"(x + 5) is exactly equal to (y - 3)",
|
248
|
+
"`first` is identical to `second`",
|
249
|
+
"result is not strictly equal to empty",
|
250
|
+
]
|
251
|
+
|
252
|
+
for source in test_cases:
|
253
|
+
parser = Parser()
|
254
|
+
program = parser.parse(source, check_semantics=False)
|
255
|
+
|
256
|
+
# Should parse without errors
|
257
|
+
assert len(parser.errors) == 0, f"Parser errors for '{source}': {parser.errors}"
|
258
|
+
assert len(program.statements) == 1
|
@@ -0,0 +1,217 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from machine_dialect.parser.symbol_table import SymbolTable, VariableInfo
|
4
|
+
|
5
|
+
|
6
|
+
class TestVariableInfo:
|
7
|
+
"""Test VariableInfo class."""
|
8
|
+
|
9
|
+
def test_single_type(self) -> None:
|
10
|
+
"""Test variable with single type."""
|
11
|
+
info = VariableInfo(["Whole Number"])
|
12
|
+
assert info.allows_type("Whole Number")
|
13
|
+
assert not info.allows_type("Text")
|
14
|
+
assert str(info) == "VariableInfo(types=Whole Number, uninitialized)"
|
15
|
+
|
16
|
+
def test_union_types(self) -> None:
|
17
|
+
"""Test variable with union types."""
|
18
|
+
info = VariableInfo(["Whole Number", "Text", "Yes/No"])
|
19
|
+
assert info.allows_type("Whole Number")
|
20
|
+
assert info.allows_type("Text")
|
21
|
+
assert info.allows_type("Yes/No")
|
22
|
+
assert not info.allows_type("Float")
|
23
|
+
|
24
|
+
def test_initialized_status(self) -> None:
|
25
|
+
"""Test initialized status tracking."""
|
26
|
+
info = VariableInfo(["Text"], initialized=False)
|
27
|
+
assert str(info) == "VariableInfo(types=Text, uninitialized)"
|
28
|
+
|
29
|
+
info.initialized = True
|
30
|
+
assert str(info) == "VariableInfo(types=Text, initialized)"
|
31
|
+
|
32
|
+
def test_definition_location(self) -> None:
|
33
|
+
"""Test that definition location is tracked."""
|
34
|
+
info = VariableInfo(["Whole Number"], definition_line=10, definition_pos=5)
|
35
|
+
assert info.definition_line == 10
|
36
|
+
assert info.definition_pos == 5
|
37
|
+
|
38
|
+
|
39
|
+
class TestSymbolTable:
|
40
|
+
"""Test SymbolTable class."""
|
41
|
+
|
42
|
+
def test_define_and_lookup(self) -> None:
|
43
|
+
"""Test defining and looking up variables."""
|
44
|
+
table = SymbolTable()
|
45
|
+
|
46
|
+
# Define a variable
|
47
|
+
table.define("count", ["Whole Number"], line=1, position=5)
|
48
|
+
|
49
|
+
# Look it up
|
50
|
+
info = table.lookup("count")
|
51
|
+
assert info is not None
|
52
|
+
assert info.type_spec == ["Whole Number"]
|
53
|
+
assert not info.initialized
|
54
|
+
|
55
|
+
def test_redefinition_error(self) -> None:
|
56
|
+
"""Test that redefinition raises error."""
|
57
|
+
table = SymbolTable()
|
58
|
+
|
59
|
+
# First definition should succeed
|
60
|
+
table.define("x", ["Whole Number"], line=1, position=1)
|
61
|
+
|
62
|
+
# Second definition should raise error
|
63
|
+
with pytest.raises(NameError) as exc_info:
|
64
|
+
table.define("x", ["Text"], line=2, position=1)
|
65
|
+
|
66
|
+
assert "already defined" in str(exc_info.value)
|
67
|
+
assert "line 1" in str(exc_info.value)
|
68
|
+
|
69
|
+
def test_lookup_undefined(self) -> None:
|
70
|
+
"""Test looking up undefined variable returns None."""
|
71
|
+
table = SymbolTable()
|
72
|
+
info = table.lookup("undefined")
|
73
|
+
assert info is None
|
74
|
+
|
75
|
+
def test_mark_initialized(self) -> None:
|
76
|
+
"""Test marking variable as initialized."""
|
77
|
+
table = SymbolTable()
|
78
|
+
table.define("message", ["Text"])
|
79
|
+
|
80
|
+
# Should be uninitialized at first
|
81
|
+
info = table.lookup("message")
|
82
|
+
assert info is not None
|
83
|
+
assert not info.initialized
|
84
|
+
|
85
|
+
# Mark as initialized
|
86
|
+
table.mark_initialized("message")
|
87
|
+
|
88
|
+
# Should now be initialized
|
89
|
+
info = table.lookup("message")
|
90
|
+
assert info is not None
|
91
|
+
assert info.initialized
|
92
|
+
|
93
|
+
def test_mark_undefined_raises_error(self) -> None:
|
94
|
+
"""Test marking undefined variable raises error."""
|
95
|
+
table = SymbolTable()
|
96
|
+
|
97
|
+
with pytest.raises(NameError) as exc_info:
|
98
|
+
table.mark_initialized("undefined")
|
99
|
+
|
100
|
+
assert "not defined" in str(exc_info.value)
|
101
|
+
|
102
|
+
def test_nested_scopes(self) -> None:
|
103
|
+
"""Test nested scope handling."""
|
104
|
+
# Global scope
|
105
|
+
global_table = SymbolTable()
|
106
|
+
global_table.define("global_var", ["Whole Number"])
|
107
|
+
|
108
|
+
# Enter function scope
|
109
|
+
func_table = global_table.enter_scope()
|
110
|
+
func_table.define("local_var", ["Text"])
|
111
|
+
|
112
|
+
# Can see both variables from inner scope
|
113
|
+
assert func_table.lookup("global_var") is not None
|
114
|
+
assert func_table.lookup("local_var") is not None
|
115
|
+
|
116
|
+
# Can only see global from outer scope
|
117
|
+
assert global_table.lookup("global_var") is not None
|
118
|
+
assert global_table.lookup("local_var") is None
|
119
|
+
|
120
|
+
def test_exit_scope(self) -> None:
|
121
|
+
"""Test exiting scope returns parent."""
|
122
|
+
global_table = SymbolTable()
|
123
|
+
func_table = global_table.enter_scope()
|
124
|
+
|
125
|
+
# Exit should return to parent
|
126
|
+
parent = func_table.exit_scope()
|
127
|
+
assert parent is global_table
|
128
|
+
|
129
|
+
# Exit from global should return None
|
130
|
+
assert global_table.exit_scope() is None
|
131
|
+
|
132
|
+
def test_is_defined_in_current_scope(self) -> None:
|
133
|
+
"""Test checking if variable is in current scope only."""
|
134
|
+
global_table = SymbolTable()
|
135
|
+
global_table.define("x", ["Whole Number"])
|
136
|
+
|
137
|
+
local_table = global_table.enter_scope()
|
138
|
+
local_table.define("y", ["Text"])
|
139
|
+
|
140
|
+
# x is not in local scope (it's in parent)
|
141
|
+
assert not local_table.is_defined_in_current_scope("x")
|
142
|
+
# y is in local scope
|
143
|
+
assert local_table.is_defined_in_current_scope("y")
|
144
|
+
# x is in global scope
|
145
|
+
assert global_table.is_defined_in_current_scope("x")
|
146
|
+
|
147
|
+
def test_mark_initialized_in_parent_scope(self) -> None:
|
148
|
+
"""Test marking variable in parent scope as initialized."""
|
149
|
+
global_table = SymbolTable()
|
150
|
+
global_table.define("global_var", ["Whole Number"])
|
151
|
+
|
152
|
+
# Enter nested scope
|
153
|
+
local_table = global_table.enter_scope()
|
154
|
+
|
155
|
+
# Mark parent's variable as initialized from child scope
|
156
|
+
local_table.mark_initialized("global_var")
|
157
|
+
|
158
|
+
# Check it's marked in the parent
|
159
|
+
info = global_table.lookup("global_var")
|
160
|
+
assert info is not None
|
161
|
+
assert info.initialized
|
162
|
+
|
163
|
+
def test_multiple_nested_scopes(self) -> None:
|
164
|
+
"""Test multiple levels of nesting."""
|
165
|
+
level1 = SymbolTable()
|
166
|
+
level1.define("var1", ["Whole Number"])
|
167
|
+
|
168
|
+
level2 = level1.enter_scope()
|
169
|
+
level2.define("var2", ["Text"])
|
170
|
+
|
171
|
+
level3 = level2.enter_scope()
|
172
|
+
level3.define("var3", ["Yes/No"])
|
173
|
+
|
174
|
+
# Level 3 can see all variables
|
175
|
+
assert level3.lookup("var1") is not None
|
176
|
+
assert level3.lookup("var2") is not None
|
177
|
+
assert level3.lookup("var3") is not None
|
178
|
+
|
179
|
+
# Level 2 can see var1 and var2
|
180
|
+
assert level2.lookup("var1") is not None
|
181
|
+
assert level2.lookup("var2") is not None
|
182
|
+
assert level2.lookup("var3") is None
|
183
|
+
|
184
|
+
# Level 1 can only see var1
|
185
|
+
assert level1.lookup("var1") is not None
|
186
|
+
assert level1.lookup("var2") is None
|
187
|
+
assert level1.lookup("var3") is None
|
188
|
+
|
189
|
+
def test_string_representation(self) -> None:
|
190
|
+
"""Test string representation of symbol table."""
|
191
|
+
table = SymbolTable()
|
192
|
+
table.define("x", ["Whole Number"])
|
193
|
+
table.define("y", ["Text", "Number"])
|
194
|
+
|
195
|
+
str_repr = str(table)
|
196
|
+
assert "Symbol Table:" in str_repr
|
197
|
+
assert "x: VariableInfo" in str_repr
|
198
|
+
assert "y: VariableInfo" in str_repr
|
199
|
+
|
200
|
+
# Test with parent
|
201
|
+
child = table.enter_scope()
|
202
|
+
child.define("z", ["Yes/No"])
|
203
|
+
str_repr = str(child)
|
204
|
+
assert "has parent scope" in str_repr
|
205
|
+
|
206
|
+
def test_union_type_checking(self) -> None:
|
207
|
+
"""Test type checking with union types."""
|
208
|
+
table = SymbolTable()
|
209
|
+
table.define("value", ["Whole Number", "Text", "Empty"])
|
210
|
+
|
211
|
+
info = table.lookup("value")
|
212
|
+
assert info is not None
|
213
|
+
assert info.allows_type("Whole Number")
|
214
|
+
assert info.allows_type("Text")
|
215
|
+
assert info.allows_type("Empty")
|
216
|
+
assert not info.allows_type("Float")
|
217
|
+
assert not info.allows_type("Yes/No")
|
@@ -0,0 +1,209 @@
|
|
1
|
+
"""Tests for URL literal expressions in the parser."""
|
2
|
+
|
3
|
+
import machine_dialect.ast as ast
|
4
|
+
from machine_dialect.parser import Parser
|
5
|
+
|
6
|
+
|
7
|
+
class TestURLLiteralExpressions:
|
8
|
+
"""Test parsing of URL literal expressions."""
|
9
|
+
|
10
|
+
def test_parse_simple_url_literal(self) -> None:
|
11
|
+
"""Test parsing a simple URL literal."""
|
12
|
+
source = '_"https://example.com"_.'
|
13
|
+
parser = Parser()
|
14
|
+
program = parser.parse(source)
|
15
|
+
|
16
|
+
assert program is not None
|
17
|
+
assert len(program.statements) == 1
|
18
|
+
|
19
|
+
stmt = program.statements[0]
|
20
|
+
assert isinstance(stmt, ast.ExpressionStatement)
|
21
|
+
assert isinstance(stmt.expression, ast.URLLiteral)
|
22
|
+
assert stmt.expression.value == "https://example.com"
|
23
|
+
|
24
|
+
def test_parse_complex_url_literal(self) -> None:
|
25
|
+
"""Test parsing a complex URL with query parameters."""
|
26
|
+
source = '_"https://api.example.com/v1/users?id=123&active=true#profile"_.'
|
27
|
+
parser = Parser()
|
28
|
+
program = parser.parse(source)
|
29
|
+
|
30
|
+
assert program is not None
|
31
|
+
assert len(program.statements) == 1
|
32
|
+
|
33
|
+
stmt = program.statements[0]
|
34
|
+
assert isinstance(stmt, ast.ExpressionStatement)
|
35
|
+
assert isinstance(stmt.expression, ast.URLLiteral)
|
36
|
+
assert stmt.expression.value == "https://api.example.com/v1/users?id=123&active=true#profile"
|
37
|
+
|
38
|
+
def test_parse_ftp_url_literal(self) -> None:
|
39
|
+
"""Test parsing an FTP URL literal."""
|
40
|
+
source = '_"ftp://files.example.com/data.zip"_.'
|
41
|
+
parser = Parser()
|
42
|
+
program = parser.parse(source)
|
43
|
+
|
44
|
+
assert program is not None
|
45
|
+
assert len(program.statements) == 1
|
46
|
+
|
47
|
+
stmt = program.statements[0]
|
48
|
+
assert isinstance(stmt, ast.ExpressionStatement)
|
49
|
+
assert isinstance(stmt.expression, ast.URLLiteral)
|
50
|
+
assert stmt.expression.value == "ftp://files.example.com/data.zip"
|
51
|
+
|
52
|
+
def test_parse_mailto_url_literal(self) -> None:
|
53
|
+
"""Test parsing a mailto URL literal."""
|
54
|
+
source = '_"mailto:user@example.com"_.'
|
55
|
+
parser = Parser()
|
56
|
+
program = parser.parse(source)
|
57
|
+
|
58
|
+
assert program is not None
|
59
|
+
assert len(program.statements) == 1
|
60
|
+
|
61
|
+
stmt = program.statements[0]
|
62
|
+
assert isinstance(stmt, ast.ExpressionStatement)
|
63
|
+
assert isinstance(stmt.expression, ast.URLLiteral)
|
64
|
+
assert stmt.expression.value == "mailto:user@example.com"
|
65
|
+
|
66
|
+
def test_url_vs_string_distinction(self) -> None:
|
67
|
+
"""Test that URLs and regular strings are parsed as different types."""
|
68
|
+
# Parse URL
|
69
|
+
url_source = '_"https://example.com"_.'
|
70
|
+
parser1 = Parser()
|
71
|
+
url_program = parser1.parse(url_source)
|
72
|
+
|
73
|
+
assert url_program is not None
|
74
|
+
url_stmt = url_program.statements[0]
|
75
|
+
assert isinstance(url_stmt, ast.ExpressionStatement)
|
76
|
+
assert isinstance(url_stmt.expression, ast.URLLiteral)
|
77
|
+
|
78
|
+
# Parse regular string
|
79
|
+
string_source = '_"not a url"_.'
|
80
|
+
parser2 = Parser()
|
81
|
+
string_program = parser2.parse(string_source)
|
82
|
+
|
83
|
+
assert string_program is not None
|
84
|
+
string_stmt = string_program.statements[0]
|
85
|
+
assert isinstance(string_stmt, ast.ExpressionStatement)
|
86
|
+
assert isinstance(string_stmt.expression, ast.StringLiteral)
|
87
|
+
|
88
|
+
def test_url_in_set_statement(self) -> None:
|
89
|
+
"""Test using a URL literal in a set statement."""
|
90
|
+
source = 'Set `api_endpoint` to _"https://api.example.com/v1"_.'
|
91
|
+
parser = Parser()
|
92
|
+
program = parser.parse(source)
|
93
|
+
|
94
|
+
assert program is not None
|
95
|
+
assert len(program.statements) == 1
|
96
|
+
|
97
|
+
stmt = program.statements[0]
|
98
|
+
assert isinstance(stmt, ast.SetStatement)
|
99
|
+
assert stmt.name is not None
|
100
|
+
assert stmt.name.value == "api_endpoint"
|
101
|
+
assert isinstance(stmt.value, ast.URLLiteral)
|
102
|
+
assert stmt.value.value == "https://api.example.com/v1"
|
103
|
+
|
104
|
+
def test_url_with_port(self) -> None:
|
105
|
+
"""Test parsing URL with port number."""
|
106
|
+
source = '_"http://localhost:8080/api"_.'
|
107
|
+
parser = Parser()
|
108
|
+
program = parser.parse(source)
|
109
|
+
|
110
|
+
assert program is not None
|
111
|
+
assert len(program.statements) == 1
|
112
|
+
|
113
|
+
stmt = program.statements[0]
|
114
|
+
assert isinstance(stmt, ast.ExpressionStatement)
|
115
|
+
assert isinstance(stmt.expression, ast.URLLiteral)
|
116
|
+
assert stmt.expression.value == "http://localhost:8080/api"
|
117
|
+
|
118
|
+
def test_data_url(self) -> None:
|
119
|
+
"""Test parsing data URL."""
|
120
|
+
source = '_"data:text/plain;base64,SGVsbG8="_.'
|
121
|
+
parser = Parser()
|
122
|
+
program = parser.parse(source)
|
123
|
+
|
124
|
+
assert program is not None
|
125
|
+
assert len(program.statements) == 1
|
126
|
+
|
127
|
+
stmt = program.statements[0]
|
128
|
+
assert isinstance(stmt, ast.ExpressionStatement)
|
129
|
+
assert isinstance(stmt.expression, ast.URLLiteral)
|
130
|
+
assert stmt.expression.value == "data:text/plain;base64,SGVsbG8="
|
131
|
+
|
132
|
+
def test_multiple_urls_in_program(self) -> None:
|
133
|
+
"""Test parsing multiple URLs in a program."""
|
134
|
+
source = """
|
135
|
+
Set `primary` to _"https://primary.example.com"_.
|
136
|
+
Set `secondary` to _"https://secondary.example.com"_.
|
137
|
+
Set `message` to _"Hello, World!"_.
|
138
|
+
"""
|
139
|
+
parser = Parser()
|
140
|
+
program = parser.parse(source)
|
141
|
+
|
142
|
+
assert program is not None
|
143
|
+
assert len(program.statements) == 3
|
144
|
+
|
145
|
+
# First statement - URL
|
146
|
+
stmt1 = program.statements[0]
|
147
|
+
assert isinstance(stmt1, ast.SetStatement)
|
148
|
+
assert isinstance(stmt1.value, ast.URLLiteral)
|
149
|
+
assert stmt1.value.value == "https://primary.example.com"
|
150
|
+
|
151
|
+
# Second statement - URL
|
152
|
+
stmt2 = program.statements[1]
|
153
|
+
assert isinstance(stmt2, ast.SetStatement)
|
154
|
+
assert isinstance(stmt2.value, ast.URLLiteral)
|
155
|
+
assert stmt2.value.value == "https://secondary.example.com"
|
156
|
+
|
157
|
+
# Third statement - Regular string
|
158
|
+
stmt3 = program.statements[2]
|
159
|
+
assert isinstance(stmt3, ast.SetStatement)
|
160
|
+
assert isinstance(stmt3.value, ast.StringLiteral)
|
161
|
+
assert stmt3.value.value == "Hello, World!"
|
162
|
+
|
163
|
+
def test_url_string_representation(self) -> None:
|
164
|
+
"""Test the string representation of URL literals."""
|
165
|
+
source = '_"https://example.com"_.'
|
166
|
+
parser = Parser()
|
167
|
+
program = parser.parse(source)
|
168
|
+
|
169
|
+
assert program is not None
|
170
|
+
stmt = program.statements[0]
|
171
|
+
assert isinstance(stmt, ast.ExpressionStatement)
|
172
|
+
|
173
|
+
# The string representation should use underscore syntax
|
174
|
+
assert str(stmt.expression) == '_"https://example.com"_'
|
175
|
+
|
176
|
+
def test_url_in_call_statement(self) -> None:
|
177
|
+
"""Test using a URL literal as an argument in a use statement."""
|
178
|
+
source = 'Use `fetch` with _"https://api.example.com/data"_.'
|
179
|
+
parser = Parser()
|
180
|
+
program = parser.parse(source)
|
181
|
+
|
182
|
+
assert program is not None
|
183
|
+
assert len(program.statements) == 1
|
184
|
+
|
185
|
+
stmt = program.statements[0]
|
186
|
+
assert isinstance(stmt, ast.CallStatement)
|
187
|
+
assert stmt.function_name is not None
|
188
|
+
assert isinstance(stmt.function_name, ast.Identifier)
|
189
|
+
assert stmt.function_name.value == "fetch"
|
190
|
+
assert stmt.arguments is not None
|
191
|
+
assert isinstance(stmt.arguments, ast.Arguments)
|
192
|
+
assert len(stmt.arguments.positional) == 1
|
193
|
+
assert isinstance(stmt.arguments.positional[0], ast.URLLiteral)
|
194
|
+
assert stmt.arguments.positional[0].value == "https://api.example.com/data"
|
195
|
+
|
196
|
+
def test_url_without_scheme_is_string(self) -> None:
|
197
|
+
"""Test that URLs without schemes are parsed as regular strings."""
|
198
|
+
source = '_"example.com"_.'
|
199
|
+
parser = Parser()
|
200
|
+
program = parser.parse(source)
|
201
|
+
|
202
|
+
assert program is not None
|
203
|
+
assert len(program.statements) == 1
|
204
|
+
|
205
|
+
stmt = program.statements[0]
|
206
|
+
assert isinstance(stmt, ast.ExpressionStatement)
|
207
|
+
# Without a scheme, it should be a StringLiteral, not URLLiteral
|
208
|
+
assert isinstance(stmt.expression, ast.StringLiteral)
|
209
|
+
assert stmt.expression.value == "example.com"
|