math-solver / solver /dsl_parser.py
Cuong2004
Deploy API from GitHub Actions
395651c
Raw
History Blame Contribute Delete
10.3 kB
import re
import logging
from typing import List, Tuple, Dict, Any
from .models import Point, Constraint
logger = logging.getLogger(__name__)
class DSLParser:
def parse(self, text: str) -> Tuple[List[Point], List[Constraint], bool]:
"""Parse DSL text into points and constraints. Stateless per call."""
points: Dict[str, Point] = {}
explicit_point_ids: List[str] = []
constraints: List[Constraint] = []
polygon_order: List[str] = []
circles: List[Dict[str, Any]] = []
segments: List[List[str]] = []
lines_ext: List[List[str]] = []
rays: List[List[str]] = []
is_3d = False
logger.info("==[DSLParser] Parsing DSL input==")
logger.debug(f"[DSLParser] Raw DSL:\n{text}")
lines = text.strip().split('\n')
for line in lines:
line = line.strip()
if not line or line.startswith('//') or line.startswith('#'):
continue
# POINT(A) or POINT(A, 0, 0, 5)
m = re.match(r'POINT\((\w+)(?:,\s*([\d\.-]+),\s*([\d\.-]+)(?:,\s*([\d\.-]+))?)?\)', line)
if m:
name = m.group(1)
x = float(m.group(2)) if m.group(2) else None
y = float(m.group(3)) if m.group(3) else None
z = float(m.group(4)) if m.group(4) else None
# z=0 with x,y is still the xy-plane; only treat as 3D when z is meaningfully non-zero.
# Otherwise POINT(A,0,0,0) incorrectly forced is_3d and broke 2D engine paths.
if z is not None and abs(z) > 1e-9:
is_3d = True
points[name] = Point(id=name, x=x, y=y, z=z)
if name not in explicit_point_ids:
explicit_point_ids.append(name)
logger.debug(f"[DSLParser] + POINT: {name} ({x}, {y}, {z})")
continue
# LENGTH(AB, 5)
m = re.match(r'LENGTH\((\w+),\s*([\d\.]+)\)', line)
if m:
target, value = m.group(1), float(m.group(2))
pts = [target[i:i+1] for i in range(len(target))]
constraints.append(Constraint(type='length', targets=pts, value=value))
logger.debug(f"[DSLParser] + LENGTH: {pts} = {value}")
continue
# ANGLE(A, 90) or ANGLE(A, 90deg)
m = re.match(r'ANGLE\((\w+),\s*([\d\.]+)(?:deg)?\)', line)
if m:
target, value = m.group(1), float(m.group(2))
constraints.append(Constraint(type='angle', targets=[target], value=value))
logger.debug(f"[DSLParser] + ANGLE: vertex={target}, degrees={value}")
continue
# PARALLEL(AB, CD)
m = re.match(r'PARALLEL\((\w+),\s*(\w+)\)', line)
if m:
seg1, seg2 = m.group(1), m.group(2)
constraints.append(Constraint(type='parallel', targets=list(seg1) + list(seg2), value=0))
logger.debug(f"[DSLParser] + PARALLEL: {seg1} || {seg2}")
continue
# PERPENDICULAR(AB, CD)
m = re.match(r'PERPENDICULAR\((\w+),\s*(\w+)\)', line)
if m:
seg1, seg2 = m.group(1), m.group(2)
constraints.append(Constraint(type='perpendicular', targets=list(seg1) + list(seg2), value=0))
logger.debug(f"[DSLParser] + PERPENDICULAR: {seg1} _|_ {seg2}")
continue
# MIDPOINT(M, AB) — M is midpoint of AB
m = re.match(r'MIDPOINT\((\w+),\s*(\w+)\)', line)
if m:
mid, seg = m.group(1), m.group(2)
if mid not in points:
points[mid] = Point(id=mid)
pts = [mid] + [seg[i:i+1] for i in range(len(seg))]
constraints.append(Constraint(type='midpoint', targets=pts, value=0))
logger.debug(f"[DSLParser] + MIDPOINT: {mid} = mid({seg})")
continue
# SECTION(E, A, C, 0.66) — E lies on AC s.t. AE = 0.66 * AC
m = re.match(r'SECTION\((\w+),\s*(\w+),\s*(\w+),\s*([\d\.-]+)\)', line)
if m:
target, p1, p2, k = m.group(1), m.group(2), m.group(3), float(m.group(4))
if target not in points:
points[target] = Point(id=target)
constraints.append(Constraint(type='section', targets=[target, p1, p2], value=k))
logger.debug(f"[DSLParser] + SECTION: {target} = {p1} + {k}({p2}-{p1})")
continue
# CIRCLE(O, r)
m = re.match(r'CIRCLE\((\w+),\s*([\d\.]+)\)', line)
if m:
center, radius = m.group(1), float(m.group(2))
if center not in points:
points[center] = Point(id=center)
constraints.append(Constraint(type='circle', targets=[center], value=radius))
circles.append({"center": center, "radius": radius})
logger.debug(f"[DSLParser] + CIRCLE: center={center}, r={radius}")
continue
# POLYGON_ORDER(A, B, C, D) — thứ tự nối điểm để vẽ đa giác
m = re.match(r'POLYGON_ORDER\(([^)]+)\)', line)
if m:
polygon_order = [p.strip() for p in m.group(1).split(',')]
logger.debug(f"[DSLParser] + POLYGON_ORDER: {polygon_order}")
continue
# SEGMENT(M, N) — đoạn thẳng phụ cần vẽ
m = re.match(r'SEGMENT\((\w+),\s*(\w+)\)', line)
if m:
p1, p2 = m.group(1), m.group(2)
segments.append([p1, p2])
constraints.append(Constraint(type='segment', targets=[p1, p2], value=0))
logger.debug(f"[DSLParser] + SEGMENT: {p1}{p2}")
continue
# LINE(A, B) — infinite line
m = re.match(r'LINE\((\w+),\s*(\w+)\)', line)
if m:
p1, p2 = m.group(1), m.group(2)
lines_ext.append([p1, p2])
constraints.append(Constraint(type='line', targets=[p1, p2], value=0))
logger.debug(f"[DSLParser] + LINE: {p1}-{p2}")
continue
# RAY(A, B) — ray AB starting at A
m = re.match(r'RAY\((\w+),\s*(\w+)\)', line)
if m:
p1, p2 = m.group(1), m.group(2)
rays.append([p1, p2])
constraints.append(Constraint(type='ray', targets=[p1, p2], value=0))
logger.debug(f"[DSLParser] + RAY: {p1}->{p2}")
continue
# TRIANGLE(ABC) / PYRAMID(S_ABCD) / PRISM(ABC_DEF)
m = re.match(r'(TRIANGLE|PYRAMID|PRISM)\(([^)]+)\)', line)
if m:
pt_type = m.group(1)
targets = m.group(2)
if pt_type in ["PYRAMID", "PRISM"]:
is_3d = True
if pt_type == "TRIANGLE":
if not polygon_order: polygon_order = list(targets)
elif pt_type == "PYRAMID":
# S_ABCD -> S is apex, ABCD is base
if "_" in targets:
apex, base = targets.split("_")
# Add segments from apex to all base points
for p in base:
segments.append([apex, p])
constraints.append(Constraint(type='segment', targets=[apex, p], value=0))
if not polygon_order: polygon_order = list(base)
elif pt_type == "PRISM":
# ABC_DEF -> two bases
if "_" in targets:
b1, b2 = targets.split("_")
for p1, p2 in zip(b1, b2):
segments.append([p1, p2])
constraints.append(Constraint(type='segment', targets=[p1, p2], value=0))
logger.debug(f"[DSLParser] + {pt_type}: {targets}")
continue
# SPHERE(O, r)
m = re.match(r'SPHERE\((\w+),\s*([\d\.]+)\)', line)
if m:
is_3d = True
center, radius = m.group(1), float(m.group(2))
if center not in points:
points[center] = Point(id=center)
constraints.append(Constraint(type='sphere', targets=[center], value=radius))
logger.debug(f"[DSLParser] + SPHERE: center={center}, r={radius}")
continue
logger.warning(f"[DSLParser] ? Unrecognized DSL line: '{line}'")
logger.info(
"[DSLParser] Parsed %d points, %d constraints, is_3d=%s.",
len(points),
len(constraints),
is_3d,
)
# Safety sweep: Ensure all points referenced in constraints actually exist in the points dictionary
for c in constraints:
for pid in c.targets:
# Some targets might be values or comma-separated strings (handled elsewhere),
# but most are single-character point IDs.
if isinstance(pid, str) and len(pid) == 1 and pid not in points:
points[pid] = Point(id=pid)
logger.debug(f"[DSLParser] ! Auto-declared missing point from constraint: {pid}")
# Attach metadata to a synthetic constraint for downstream use
if polygon_order:
constraints.append(Constraint(type='polygon_order', targets=polygon_order, value=0))
elif explicit_point_ids:
# Re-use polygon_order as a carrier for explicit points IF no real order was specified
constraints.append(Constraint(type='explicit_points', targets=explicit_point_ids, value=0))
# Add auxiliary metadata for lines and rays
if lines_ext:
constraints.append(Constraint(type='lines_metadata', targets=[",".join(l) for l in lines_ext], value=0))
if rays:
constraints.append(Constraint(type='rays_metadata', targets=[",".join(l) for l in rays], value=0))
return list(points.values()), constraints, is_3d