Commit 41f07888 authored by Roman Alifanov's avatar Roman Alifanov

Add ..= string concat operator, fix constructor and resolver bugs

Add string concatenation assignment operator (..=) across the full pipeline: lexer, parser, and backend codegen for variables, this.field, dict fields, and object fields. Fix __ct_last_instance set after construct call instead of before. Fix IR builder treating field symbols as known function calls. Fix resolver _current_func corruption during constructor resolution.
parent aa00c122
...@@ -54,13 +54,13 @@ def _emit_constructor_func(cls: IRClass, ctx: 'EmitContext') -> None: ...@@ -54,13 +54,13 @@ def _emit_constructor_func(cls: IRClass, ctx: 'EmitContext') -> None:
# Generate unique instance ID # Generate unique instance ID
ctx.emit(f'local __ct_id="{cls.name}_$$_$RANDOM"') ctx.emit(f'local __ct_id="{cls.name}_$$_$RANDOM"')
ctx.emit(f'__ct_obj_class["${{__ct_id}}"]={cls.name!r}') ctx.emit(f'__ct_obj_class["${{__ct_id}}"]={cls.name!r}')
ctx.emit(f'__ct_last_instance="$__ct_id"')
# Initialise fields # Initialise fields
for f in cls.fields: for f in cls.fields:
_init_field(cls.name, f, ctx) _init_field(cls.name, f, ctx)
# Call user-defined construct if present, passing all args # Call user-defined construct if present, passing all args
if cls.constructor: if cls.constructor:
ctx.emit(f'{CLASS_FUNC_PREFIX}{cls.name}_construct "${{__ct_id}}" "$@"') ctx.emit(f'{CLASS_FUNC_PREFIX}{cls.name}_construct "${{__ct_id}}" "$@"')
ctx.emit(f'__ct_last_instance="$__ct_id"')
ctx.emit('}') ctx.emit('}')
ctx.emit('') ctx.emit('')
......
...@@ -236,6 +236,13 @@ def _assign(node: IRAssign, ctx: 'EmitContext') -> None: ...@@ -236,6 +236,13 @@ def _assign(node: IRAssign, ctx: 'EmitContext') -> None:
ctx.emit(f'export {target}={val}') ctx.emit(f'export {target}={val}')
return return
# String concatenation assignment
if op == '..=':
val = _expr(value, ctx)
val_inner = val.strip('"')
ctx.emit(f'{target}="${{{target}}}{val_inner}"')
return
# Augmented assignment with arithmetic # Augmented assignment with arithmetic
if op in ('+=', '-=', '*=', '/=', '%='): if op in ('+=', '-=', '*=', '/=', '%='):
val = _expr(value, ctx) val = _expr(value, ctx)
...@@ -285,7 +292,11 @@ def _field_assign(node: IRFieldAssign, ctx: 'EmitContext') -> None: ...@@ -285,7 +292,11 @@ def _field_assign(node: IRFieldAssign, ctx: 'EmitContext') -> None:
if isinstance(node.receiver, IRThis): if isinstance(node.receiver, IRThis):
key = f'$__ct_this.{node.field_name}' key = f'$__ct_this.{node.field_name}'
if op in ('+=', '-=', '*=', '/='): if op == '..=':
existing = f'${{{OBJ_STORE}["$__ct_this.{node.field_name}"]}}'
val_inner = val.strip('"')
ctx.emit(f'{OBJ_STORE}["$__ct_this.{node.field_name}"]="{existing}{val_inner}"')
elif op in ('+=', '-=', '*=', '/='):
existing = f'"${{{OBJ_STORE}["$__ct_this.{node.field_name}"]}}"' existing = f'"${{{OBJ_STORE}["$__ct_this.{node.field_name}"]}}"'
ctx.emit(f'{OBJ_STORE}["$__ct_this.{node.field_name}"]="$(( {existing} {op[0]} {val} ))"') ctx.emit(f'{OBJ_STORE}["$__ct_this.{node.field_name}"]="$(( {existing} {op[0]} {val} ))"')
else: else:
...@@ -296,13 +307,21 @@ def _field_assign(node: IRFieldAssign, ctx: 'EmitContext') -> None: ...@@ -296,13 +307,21 @@ def _field_assign(node: IRFieldAssign, ctx: 'EmitContext') -> None:
recv_name = recv.strip('"').strip('${}') recv_name = recv.strip('"').strip('${}')
# Dict-as-struct: if receiver is a known dict var, use dict subscript syntax # Dict-as-struct: if receiver is a known dict var, use dict subscript syntax
if recv_name in ctx.dict_vars or node.receiver.type.kind == 'dict': if recv_name in ctx.dict_vars or node.receiver.type.kind == 'dict':
if op in ('+=', '-=', '*=', '/='): if op == '..=':
existing = f'${{{recv_name}["{node.field_name}"]}}'
val_inner = val.strip('"')
ctx.emit(f'{recv_name}["{node.field_name}"]="{existing}{val_inner}"')
elif op in ('+=', '-=', '*=', '/='):
existing = f'"${{{recv_name}["{node.field_name}"]}}"' existing = f'"${{{recv_name}["{node.field_name}"]}}"'
ctx.emit(f'{recv_name}["{node.field_name}"]="$(( {existing} {op[0]} {val} ))"') ctx.emit(f'{recv_name}["{node.field_name}"]="$(( {existing} {op[0]} {val} ))"')
else: else:
ctx.emit(f'{recv_name}["{node.field_name}"]={val}') ctx.emit(f'{recv_name}["{node.field_name}"]={val}')
return return
if op in ('+=', '-=', '*=', '/='): if op == '..=':
existing = f'${{{OBJ_STORE}["${{{recv_name}}}.{node.field_name}"]}}'
val_inner = val.strip('"')
ctx.emit(f'{OBJ_STORE}["${{{recv_name}}}.{node.field_name}"]="{existing}{val_inner}"')
elif op in ('+=', '-=', '*=', '/='):
existing = f'"${{{OBJ_STORE}["${{{recv_name}}}.{node.field_name}"]}}"' existing = f'"${{{OBJ_STORE}["${{{recv_name}}}.{node.field_name}"]}}"'
ctx.emit(f'{OBJ_STORE}["${{{recv_name}}}.{node.field_name}"]="$(( {existing} {op[0]} {val} ))"') ctx.emit(f'{OBJ_STORE}["${{{recv_name}}}.{node.field_name}"]="$(( {existing} {op[0]} {val} ))"')
else: else:
......
...@@ -597,7 +597,7 @@ class IRBuilder: ...@@ -597,7 +597,7 @@ class IRBuilder:
name = callee.name name = callee.name
sym = self._resolve_name(name) sym = self._resolve_name(name)
if sym is not None: if sym is not None and sym.kind != 'field':
return IRCall(callee=sym, callee_name=name, args=args, return IRCall(callee=sym, callee_name=name, args=args,
is_shell_cmd=False, source=loc) is_shell_cmd=False, source=loc)
......
...@@ -328,12 +328,16 @@ class Lexer: ...@@ -328,12 +328,16 @@ class Lexer:
tokens.append(self._tok(TokenType.SLASH_ASSIGN, '/=', line, col)) tokens.append(self._tok(TokenType.SLASH_ASSIGN, '/=', line, col))
continue continue
# --- Dot variants (order: ... > .. > .) --- # --- Dot variants (order: ... > ..= > .. > .) ---
if ch == '.': if ch == '.':
if self._ch(1) == '.' and self._ch(2) == '.': if self._ch(1) == '.' and self._ch(2) == '.':
self._advance(); self._advance(); self._advance() self._advance(); self._advance(); self._advance()
tokens.append(self._tok(TokenType.DOTDOTDOT, '...', line, col)) tokens.append(self._tok(TokenType.DOTDOTDOT, '...', line, col))
self._dot_at_eol = False self._dot_at_eol = False
elif self._ch(1) == '.' and self._ch(2) == '=':
self._advance(); self._advance(); self._advance()
tokens.append(self._tok(TokenType.DOTDOT_ASSIGN, '..=', line, col))
self._dot_at_eol = False
elif self._ch(1) == '.': elif self._ch(1) == '.':
self._advance(); self._advance() self._advance(); self._advance()
tokens.append(self._tok(TokenType.DOTDOT, '..', line, col)) tokens.append(self._tok(TokenType.DOTDOT, '..', line, col))
......
...@@ -67,6 +67,7 @@ class TokenType(Enum): ...@@ -67,6 +67,7 @@ class TokenType(Enum):
MINUS_ASSIGN = auto() MINUS_ASSIGN = auto()
STAR_ASSIGN = auto() STAR_ASSIGN = auto()
SLASH_ASSIGN = auto() SLASH_ASSIGN = auto()
DOTDOT_ASSIGN = auto()
DOT = auto() DOT = auto()
DOTDOT = auto() DOTDOT = auto()
DOTDOTDOT = auto() DOTDOTDOT = auto()
......
...@@ -336,7 +336,10 @@ class Resolver: ...@@ -336,7 +336,10 @@ class Resolver:
# Resolve constructor # Resolve constructor
if node.constructor: if node.constructor:
saved_func = self._current_func
self._current_func = cls_sym
self._resolve_constructor(node.constructor, cls_scope) self._resolve_constructor(node.constructor, cls_scope)
self._current_func = saved_func
# Resolve methods # Resolve methods
for method in node.methods: for method in node.methods:
......
...@@ -703,7 +703,7 @@ class Parser: ...@@ -703,7 +703,7 @@ class Parser:
if self._check(TokenType.ASSIGN, TokenType.PLUS_ASSIGN, if self._check(TokenType.ASSIGN, TokenType.PLUS_ASSIGN,
TokenType.MINUS_ASSIGN, TokenType.STAR_ASSIGN, TokenType.MINUS_ASSIGN, TokenType.STAR_ASSIGN,
TokenType.SLASH_ASSIGN): TokenType.SLASH_ASSIGN, TokenType.DOTDOT_ASSIGN):
op = self._advance().value op = self._advance().value
value = self.parse_expression() value = self.parse_expression()
return Assignment(target=expr, type_annotation=type_annotation, return Assignment(target=expr, type_annotation=type_annotation,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment