2424from .syntax import (
2525 BooleanSyntax , ComparisonSyntax , ComputeCommandSyntax , ExecuteCommandSyntax ,
2626 DeleteVariablesCommandSyntax , FormatsCommandSyntax , IfCommandSyntax ,
27- OperandSyntax , PredicateSyntax ,
27+ NotSyntax , OperandSyntax , PredicateSyntax , Token , VariableRangeSyntax ,
2828 RecodeCommandSyntax , RecodeMatchSyntax , RecodeResultSyntax ,
2929 SpssSyntaxProgram , SyntaxLiteral , ValueLabelsCommandSyntax ,
3030 StringCommandSyntax , VariableLabelsCommandSyntax , VariableLevelCommandSyntax ,
@@ -74,9 +74,16 @@ def _bind_operand(
7474
7575
7676def _bind_predicate (
77- syntax : PredicateSyntax , variables : list [VariableDefinition ],
77+ syntax : PredicateSyntax , variables : list [VariableDefinition ], * , negated : bool = False ,
7878) -> PredicateExpression :
79+ if isinstance (syntax , NotSyntax ):
80+ return _bind_predicate (syntax .operand , variables , negated = not negated )
7981 if isinstance (syntax , ComparisonSyntax ):
82+ if syntax .operator == "ne" or (negated and syntax .operator == "=" ):
83+ expanded = BooleanSyntax ("or" , tuple (
84+ replace (syntax , operator = operator ) for operator in ("<" , ">" )
85+ ), syntax .span )
86+ return _bind_predicate (expanded , variables , negated = negated and syntax .operator == "ne" )
8087 left , left_type = _bind_operand (syntax .left , variables )
8188 right , right_type = _bind_operand (syntax .right , variables )
8289 if left_type != right_type :
@@ -91,19 +98,21 @@ def _bind_predicate(
9198 "Ordered comparisons require numeric operands." ,
9299 span = syntax .span , operator = syntax .operator ,
93100 )
94- return ComparisonExpression (left , syntax .operator , right )
101+ operator = ({"=" : "=" , "<" : ">=" , "<=" : ">" , ">" : "<=" , ">=" : "<" }[syntax .operator ] if negated else syntax .operator )
102+ return ComparisonExpression (left , operator , right )
95103 assert isinstance (syntax , BooleanSyntax )
104+ operator = ("or" if syntax .operator == "and" else "and" ) if negated else syntax .operator
96105 operands : list [PredicateExpression ] = []
97106 for operand in syntax .operands :
98- bound = _bind_predicate (operand , variables )
107+ bound = _bind_predicate (operand , variables , negated = negated )
99108 if (
100109 isinstance (bound , BooleanExpression )
101- and bound .operator == syntax . operator
110+ and bound .operator == operator
102111 ):
103112 operands .extend (bound .operands )
104113 else :
105114 operands .append (bound )
106- return BooleanExpression (syntax . operator , tuple (operands ))
115+ return BooleanExpression (operator , tuple (operands ))
107116
108117
109118def _assignment (
@@ -151,7 +160,7 @@ def _assignment(
151160
152161
153162def _match (
154- syntax : RecodeMatchSyntax , source : VariableDefinition ,
163+ syntax : RecodeMatchSyntax , source : VariableDefinition , * , official_v03 : bool = False ,
155164) -> RecodeMatch :
156165 expected = _expected_type (source .storage_kind )
157166 if syntax .kind == "system_missing" :
@@ -172,7 +181,7 @@ def _match(
172181 )
173182 if lower .number () > upper .number ():
174183 raise frontend_error (
175- "invalid_numeric_range" ,
184+ "invalid_variable_range" if official_v03 else " invalid_numeric_range" ,
176185 "THRU lower endpoint exceeds its upper endpoint." ,
177186 span = syntax .span , variable = source .name ,
178187 )
@@ -252,10 +261,12 @@ def _validate_recode_string_width(
252261
253262
254263def _bind_recode (
255- command : RecodeCommandSyntax , variables : list [VariableDefinition ],
264+ command : RecodeCommandSyntax , variables : list [VariableDefinition ], * , official_v03 : bool = False ,
256265) -> tuple [list [RecodeOperation ], list [SourceSpan ]]:
257266 sources = [_resolve (variables , token .text , token .span )[1 ] for token in command .sources ]
258267 targets = command .targets
268+ if targets is not None and len (targets ) != len (sources ):
269+ raise frontend_error ("spss_syntax_error" , "RECODE INTO requires one target per source." , span = command .span )
259270 target_mode : Literal ["create" , "replace" ] = "create" if targets is not None else "replace"
260271 target_names = (
261272 [token .text for token in targets ] if targets is not None
@@ -292,7 +303,7 @@ def _bind_recode(
292303 if clause .match .kind == "else" :
293304 else_result = result
294305 continue
295- rules .append (RecodeRule (_match (clause .match , source ), result ))
306+ rules .append (RecodeRule (_match (clause .match , source , official_v03 = official_v03 ), result ))
296307 unmatched = else_result or RecodeResult (
297308 "system_missing" if target_mode == "create" else "copy"
298309 )
@@ -332,6 +343,28 @@ def _bind_recode(
332343 # replace intentionally preserves the existing variable metadata. A later
333344 # VALUE LABELS command replaces value labels explicitly.
334345 return operations , spans
346+
347+
348+ def _expand_variables (
349+ tokens : tuple [Token | VariableRangeSyntax , ...], variables : list [VariableDefinition ],
350+ ) -> tuple [Token , ...]:
351+ expanded : list [Token ] = []
352+ for token in tokens :
353+ if isinstance (token , VariableRangeSyntax ):
354+ first , _ = _resolve (variables , token .first .text , token .first .span )
355+ last = first
356+ for endpoint in (token .last , * token .continuations ):
357+ next_index , _ = _resolve (variables , endpoint .text , endpoint .span )
358+ if last > next_index :
359+ raise frontend_error ("invalid_variable_range" , "TO endpoints are reversed in dictionary order." , span = token .span )
360+ last = next_index
361+ expanded .extend (Token ("identifier" , v .name , v .name , token .span ) for v in variables [first :last + 1 ])
362+ else :
363+ _resolve (variables , token .text , token .span )
364+ expanded .append (token )
365+ return tuple (expanded )
366+
367+
335368def bind_spss_syntax (
336369 program : SpssSyntaxProgram , schema : VariableSchema , * , input_alias : str = "parent" ,
337370) -> BoundTransformation :
@@ -345,6 +378,22 @@ def bind_spss_syntax(
345378 operations : list [PlanOperation ] = []
346379 spans : list [SourceSpan ] = []
347380 for command in program .commands :
381+ if program .official_v03 :
382+ if isinstance (command , (StringCommandSyntax , DeleteVariablesCommandSyntax )):
383+ raise frontend_error ("unsupported_spss_command" , "Python schema commands are outside official Frontend 0.3." , span = command .span )
384+ if isinstance (command , RecodeCommandSyntax ):
385+ command = replace (command , sources = _expand_variables (command .sources , variables ))
386+ elif isinstance (command , (FormatsCommandSyntax , VariableLevelCommandSyntax , VariableLabelsCommandSyntax )):
387+ command = replace (command , assignments = tuple (
388+ replace (assignment , variable = token )
389+ for assignment in command .assignments
390+ for token in _expand_variables ((assignment .variable ,), variables )
391+ ))
392+ elif isinstance (command , ValueLabelsCommandSyntax ):
393+ command = replace (command , groups = tuple (
394+ replace (group , variables = _expand_variables (group .variables , variables ))
395+ for group in command .groups
396+ ))
348397 if isinstance (command , StringCommandSyntax ):
349398 for variable_token in command .variables :
350399 if variable_token .text .startswith ("__" ):
@@ -383,7 +432,7 @@ def bind_spss_syntax(
383432 del variables [index ]
384433 continue
385434 if isinstance (command , RecodeCommandSyntax ):
386- recodes , recode_spans = _bind_recode (command , variables )
435+ recodes , recode_spans = _bind_recode (command , variables , official_v03 = program . official_v03 )
387436 operations .extend (recodes )
388437 spans .extend (recode_spans )
389438 continue
@@ -515,6 +564,10 @@ def bind_spss_syntax(
515564 "VALUE LABELS contains duplicate canonical codes." ,
516565 span = group .span , variable = variable .name ,
517566 )
567+ if command .additive :
568+ merged = {label .value .canonical_key (): label for label in variable .value_labels }
569+ merged .update ((label .value .canonical_key (), label ) for label in labels )
570+ labels = tuple (merged .values ())
518571 operation = ReplaceValueLabelsOperation (variable .name , labels )
519572 operations .append (operation )
520573 spans .append (group .span )
0 commit comments