oscura 0.0.1__py3-none-any.whl → 0.1.1__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.
- oscura/__init__.py +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/WHEEL +0 -0
oscura/dsl/parser.py
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
"""Oscura DSL Parser.
|
|
2
|
+
|
|
3
|
+
Implements simple domain-specific language for trace analysis workflows.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum, auto
|
|
8
|
+
from typing import Any, Union
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TokenType(Enum):
|
|
12
|
+
"""Token types for DSL lexer."""
|
|
13
|
+
|
|
14
|
+
# Literals
|
|
15
|
+
STRING = auto()
|
|
16
|
+
NUMBER = auto()
|
|
17
|
+
VARIABLE = auto()
|
|
18
|
+
IDENTIFIER = auto()
|
|
19
|
+
|
|
20
|
+
# Operators
|
|
21
|
+
PIPE = auto()
|
|
22
|
+
ASSIGN = auto()
|
|
23
|
+
COMMA = auto()
|
|
24
|
+
|
|
25
|
+
# Keywords
|
|
26
|
+
LOAD = auto()
|
|
27
|
+
FILTER = auto()
|
|
28
|
+
MEASURE = auto()
|
|
29
|
+
PLOT = auto()
|
|
30
|
+
EXPORT = auto()
|
|
31
|
+
FOR = auto()
|
|
32
|
+
IN = auto()
|
|
33
|
+
GLOB = auto()
|
|
34
|
+
|
|
35
|
+
# Structural
|
|
36
|
+
LPAREN = auto()
|
|
37
|
+
RPAREN = auto()
|
|
38
|
+
COLON = auto()
|
|
39
|
+
NEWLINE = auto()
|
|
40
|
+
INDENT = auto()
|
|
41
|
+
DEDENT = auto()
|
|
42
|
+
EOF = auto()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Token:
|
|
47
|
+
"""Lexical token."""
|
|
48
|
+
|
|
49
|
+
type: TokenType
|
|
50
|
+
value: Any
|
|
51
|
+
line: int
|
|
52
|
+
column: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Lexer:
|
|
56
|
+
"""Tokenizer for Oscura DSL.
|
|
57
|
+
|
|
58
|
+
Breaks input text into tokens for parsing.
|
|
59
|
+
Supports indentation-based block structure (Python-style).
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
KEYWORDS = { # noqa: RUF012
|
|
63
|
+
"load": TokenType.LOAD,
|
|
64
|
+
"filter": TokenType.FILTER,
|
|
65
|
+
"measure": TokenType.MEASURE,
|
|
66
|
+
"plot": TokenType.PLOT,
|
|
67
|
+
"export": TokenType.EXPORT,
|
|
68
|
+
"for": TokenType.FOR,
|
|
69
|
+
"in": TokenType.IN,
|
|
70
|
+
"glob": TokenType.GLOB,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def __init__(self, text: str):
|
|
74
|
+
"""Initialize lexer with input text.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
text: DSL source code
|
|
78
|
+
"""
|
|
79
|
+
self.text = text
|
|
80
|
+
self.pos = 0
|
|
81
|
+
self.line = 1
|
|
82
|
+
self.column = 1
|
|
83
|
+
self.tokens: list[Token] = []
|
|
84
|
+
# Indentation tracking
|
|
85
|
+
self.indent_stack: list[int] = [0]
|
|
86
|
+
self.at_line_start = True
|
|
87
|
+
|
|
88
|
+
def current_char(self) -> str | None:
|
|
89
|
+
"""Get current character without advancing."""
|
|
90
|
+
if self.pos >= len(self.text):
|
|
91
|
+
return None
|
|
92
|
+
return self.text[self.pos]
|
|
93
|
+
|
|
94
|
+
def peek_char(self, offset: int = 1) -> str | None:
|
|
95
|
+
"""Peek ahead at character."""
|
|
96
|
+
pos = self.pos + offset
|
|
97
|
+
if pos >= len(self.text):
|
|
98
|
+
return None
|
|
99
|
+
return self.text[pos]
|
|
100
|
+
|
|
101
|
+
def advance(self) -> None:
|
|
102
|
+
"""Advance position and update line/column."""
|
|
103
|
+
if self.pos < len(self.text) and self.text[self.pos] == "\n":
|
|
104
|
+
self.line += 1
|
|
105
|
+
self.column = 1
|
|
106
|
+
self.at_line_start = True
|
|
107
|
+
else:
|
|
108
|
+
self.column += 1
|
|
109
|
+
self.pos += 1
|
|
110
|
+
|
|
111
|
+
def skip_whitespace(self) -> None:
|
|
112
|
+
"""Skip whitespace except newlines."""
|
|
113
|
+
while self.current_char() and self.current_char() in " \t\r": # type: ignore[operator]
|
|
114
|
+
self.advance()
|
|
115
|
+
|
|
116
|
+
def skip_comment(self) -> None:
|
|
117
|
+
"""Skip # comment to end of line."""
|
|
118
|
+
if self.current_char() == "#":
|
|
119
|
+
while self.current_char() and self.current_char() != "\n":
|
|
120
|
+
self.advance()
|
|
121
|
+
|
|
122
|
+
def measure_indent(self) -> int:
|
|
123
|
+
"""Measure indentation at current position (after newline).
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Number of spaces of indentation (tabs count as 4 spaces)
|
|
127
|
+
"""
|
|
128
|
+
indent = 0
|
|
129
|
+
start_pos = self.pos
|
|
130
|
+
|
|
131
|
+
while self.current_char() and self.current_char() in " \t": # type: ignore[operator]
|
|
132
|
+
if self.current_char() == " ":
|
|
133
|
+
indent += 1
|
|
134
|
+
elif self.current_char() == "\t":
|
|
135
|
+
indent += 4 # Tab = 4 spaces
|
|
136
|
+
self.pos += 1
|
|
137
|
+
self.column += 1
|
|
138
|
+
|
|
139
|
+
# Check if rest of line is blank or comment
|
|
140
|
+
if self.current_char() == "#" or self.current_char() == "\n" or self.current_char() is None:
|
|
141
|
+
# Blank line or comment-only line - reset position and return -1
|
|
142
|
+
self.pos = start_pos
|
|
143
|
+
self.column = 1
|
|
144
|
+
return -1 # Signal to ignore this line for indentation
|
|
145
|
+
|
|
146
|
+
return indent
|
|
147
|
+
|
|
148
|
+
def read_string(self) -> str:
|
|
149
|
+
"""Read quoted string literal."""
|
|
150
|
+
quote_char = self.current_char()
|
|
151
|
+
self.advance() # Skip opening quote
|
|
152
|
+
|
|
153
|
+
chars = []
|
|
154
|
+
while self.current_char() and self.current_char() != quote_char:
|
|
155
|
+
if self.current_char() == "\\":
|
|
156
|
+
self.advance()
|
|
157
|
+
# Simple escape sequences
|
|
158
|
+
escape_map = {"n": "\n", "t": "\t", "r": "\r", "\\": "\\", '"': '"', "'": "'"}
|
|
159
|
+
if self.current_char() in escape_map:
|
|
160
|
+
chars.append(escape_map[self.current_char()]) # type: ignore[index]
|
|
161
|
+
else:
|
|
162
|
+
chars.append(self.current_char() or "")
|
|
163
|
+
else:
|
|
164
|
+
chars.append(self.current_char() or "")
|
|
165
|
+
self.advance()
|
|
166
|
+
|
|
167
|
+
if not self.current_char():
|
|
168
|
+
raise SyntaxError(f"Unterminated string at line {self.line}")
|
|
169
|
+
|
|
170
|
+
self.advance() # Skip closing quote
|
|
171
|
+
return "".join(chars)
|
|
172
|
+
|
|
173
|
+
def read_number(self) -> int | float:
|
|
174
|
+
"""Read numeric literal."""
|
|
175
|
+
chars = []
|
|
176
|
+
has_dot = False
|
|
177
|
+
has_exp = False
|
|
178
|
+
|
|
179
|
+
while self.current_char() and (
|
|
180
|
+
self.current_char().isdigit() or self.current_char() in ".eE+-" # type: ignore[union-attr, operator]
|
|
181
|
+
):
|
|
182
|
+
if self.current_char() == ".":
|
|
183
|
+
if has_dot:
|
|
184
|
+
break
|
|
185
|
+
has_dot = True
|
|
186
|
+
elif self.current_char() in "eE": # type: ignore[operator]
|
|
187
|
+
if has_exp:
|
|
188
|
+
break
|
|
189
|
+
has_exp = True
|
|
190
|
+
chars.append(self.current_char())
|
|
191
|
+
self.advance()
|
|
192
|
+
|
|
193
|
+
num_str = "".join(chars) # type: ignore[arg-type]
|
|
194
|
+
return float(num_str) if has_dot or has_exp else int(num_str)
|
|
195
|
+
|
|
196
|
+
def read_identifier(self) -> str:
|
|
197
|
+
"""Read identifier or keyword."""
|
|
198
|
+
chars = []
|
|
199
|
+
while self.current_char() and (self.current_char().isalnum() or self.current_char() in "_"): # type: ignore[union-attr, operator, syntax, operator]
|
|
200
|
+
chars.append(self.current_char())
|
|
201
|
+
self.advance()
|
|
202
|
+
return "".join(chars) # type: ignore[arg-type]
|
|
203
|
+
|
|
204
|
+
def read_variable(self) -> str:
|
|
205
|
+
"""Read variable name ($varname)."""
|
|
206
|
+
self.advance() # Skip $
|
|
207
|
+
return "$" + self.read_identifier()
|
|
208
|
+
|
|
209
|
+
def emit_indent_tokens(self, indent: int) -> None:
|
|
210
|
+
"""Emit INDENT/DEDENT tokens based on indentation change.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
indent: Current line's indentation level
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
SyntaxError: If indentation is inconsistent.
|
|
217
|
+
"""
|
|
218
|
+
current_indent = self.indent_stack[-1]
|
|
219
|
+
|
|
220
|
+
if indent > current_indent:
|
|
221
|
+
# Increased indentation
|
|
222
|
+
self.indent_stack.append(indent)
|
|
223
|
+
self.tokens.append(Token(TokenType.INDENT, indent, self.line, 1))
|
|
224
|
+
elif indent < current_indent:
|
|
225
|
+
# Decreased indentation - may need multiple DEDENTs
|
|
226
|
+
while self.indent_stack and indent < self.indent_stack[-1]:
|
|
227
|
+
self.indent_stack.pop()
|
|
228
|
+
self.tokens.append(Token(TokenType.DEDENT, indent, self.line, 1))
|
|
229
|
+
|
|
230
|
+
# Check for inconsistent indentation
|
|
231
|
+
if self.indent_stack and indent != self.indent_stack[-1]:
|
|
232
|
+
raise SyntaxError(
|
|
233
|
+
f"Inconsistent indentation at line {self.line}: "
|
|
234
|
+
f"got {indent} spaces, expected {self.indent_stack[-1]}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def tokenize(self) -> list[Token]:
|
|
238
|
+
"""Tokenize entire input.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
List of tokens
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
SyntaxError: On lexical errors
|
|
245
|
+
"""
|
|
246
|
+
while self.pos < len(self.text):
|
|
247
|
+
# Handle indentation at line start
|
|
248
|
+
if self.at_line_start:
|
|
249
|
+
self.at_line_start = False
|
|
250
|
+
indent = self.measure_indent()
|
|
251
|
+
|
|
252
|
+
# Skip blank/comment lines
|
|
253
|
+
if indent == -1:
|
|
254
|
+
self.skip_whitespace()
|
|
255
|
+
self.skip_comment()
|
|
256
|
+
if self.current_char() == "\n":
|
|
257
|
+
self.advance()
|
|
258
|
+
continue
|
|
259
|
+
elif self.current_char() is None:
|
|
260
|
+
break
|
|
261
|
+
else:
|
|
262
|
+
self.emit_indent_tokens(indent)
|
|
263
|
+
|
|
264
|
+
self.skip_whitespace()
|
|
265
|
+
self.skip_comment()
|
|
266
|
+
|
|
267
|
+
if not self.current_char():
|
|
268
|
+
break
|
|
269
|
+
|
|
270
|
+
line, col = self.line, self.column
|
|
271
|
+
char = self.current_char()
|
|
272
|
+
|
|
273
|
+
# Newline
|
|
274
|
+
if char == "\n":
|
|
275
|
+
self.tokens.append(Token(TokenType.NEWLINE, "\n", line, col))
|
|
276
|
+
self.advance()
|
|
277
|
+
|
|
278
|
+
# String
|
|
279
|
+
elif char in "\"'": # type: ignore[operator]
|
|
280
|
+
value = self.read_string()
|
|
281
|
+
self.tokens.append(Token(TokenType.STRING, value, line, col))
|
|
282
|
+
|
|
283
|
+
# Number
|
|
284
|
+
elif char.isdigit() or ( # type: ignore[union-attr]
|
|
285
|
+
char == "." and self.peek_char() and self.peek_char().isdigit() # type: ignore[union-attr]
|
|
286
|
+
):
|
|
287
|
+
value = self.read_number() # type: ignore[assignment]
|
|
288
|
+
self.tokens.append(Token(TokenType.NUMBER, value, line, col))
|
|
289
|
+
|
|
290
|
+
# Variable
|
|
291
|
+
elif char == "$":
|
|
292
|
+
value = self.read_variable()
|
|
293
|
+
self.tokens.append(Token(TokenType.VARIABLE, value, line, col))
|
|
294
|
+
|
|
295
|
+
# Pipe
|
|
296
|
+
elif char == "|":
|
|
297
|
+
self.tokens.append(Token(TokenType.PIPE, "|", line, col))
|
|
298
|
+
self.advance()
|
|
299
|
+
|
|
300
|
+
# Assignment
|
|
301
|
+
elif char == "=":
|
|
302
|
+
self.tokens.append(Token(TokenType.ASSIGN, "=", line, col))
|
|
303
|
+
self.advance()
|
|
304
|
+
|
|
305
|
+
# Comma
|
|
306
|
+
elif char == ",":
|
|
307
|
+
self.tokens.append(Token(TokenType.COMMA, ",", line, col))
|
|
308
|
+
self.advance()
|
|
309
|
+
|
|
310
|
+
# Colon
|
|
311
|
+
elif char == ":":
|
|
312
|
+
self.tokens.append(Token(TokenType.COLON, ":", line, col))
|
|
313
|
+
self.advance()
|
|
314
|
+
|
|
315
|
+
# Parentheses
|
|
316
|
+
elif char == "(":
|
|
317
|
+
self.tokens.append(Token(TokenType.LPAREN, "(", line, col))
|
|
318
|
+
self.advance()
|
|
319
|
+
elif char == ")":
|
|
320
|
+
self.tokens.append(Token(TokenType.RPAREN, ")", line, col))
|
|
321
|
+
self.advance()
|
|
322
|
+
|
|
323
|
+
# Identifier or keyword
|
|
324
|
+
elif char.isalpha() or char == "_": # type: ignore[union-attr]
|
|
325
|
+
ident = self.read_identifier()
|
|
326
|
+
token_type = self.KEYWORDS.get(ident.lower(), TokenType.IDENTIFIER)
|
|
327
|
+
self.tokens.append(Token(token_type, ident, line, col))
|
|
328
|
+
|
|
329
|
+
else:
|
|
330
|
+
raise SyntaxError(f"Unexpected character '{char}' at line {line}, column {col}")
|
|
331
|
+
|
|
332
|
+
# Emit remaining DEDENTs at end of file
|
|
333
|
+
while len(self.indent_stack) > 1:
|
|
334
|
+
self.indent_stack.pop()
|
|
335
|
+
self.tokens.append(Token(TokenType.DEDENT, 0, self.line, self.column))
|
|
336
|
+
|
|
337
|
+
# Add EOF token
|
|
338
|
+
self.tokens.append(Token(TokenType.EOF, None, self.line, self.column))
|
|
339
|
+
return self.tokens
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass
|
|
343
|
+
class ASTNode:
|
|
344
|
+
"""Base class for AST nodes."""
|
|
345
|
+
|
|
346
|
+
line: int
|
|
347
|
+
column: int
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@dataclass
|
|
351
|
+
class Assignment(ASTNode):
|
|
352
|
+
"""Variable assignment: $var = expr."""
|
|
353
|
+
|
|
354
|
+
variable: str
|
|
355
|
+
expression: "Expression"
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@dataclass
|
|
359
|
+
class Pipeline(ASTNode):
|
|
360
|
+
"""Pipeline expression: expr | command | command."""
|
|
361
|
+
|
|
362
|
+
stages: list["Expression"]
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@dataclass
|
|
366
|
+
class Command(ASTNode):
|
|
367
|
+
"""Command invocation: command arg1 arg2."""
|
|
368
|
+
|
|
369
|
+
name: str
|
|
370
|
+
args: list["Expression"]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@dataclass
|
|
374
|
+
class FunctionCall(ASTNode):
|
|
375
|
+
"""Function call: func(arg1, arg2)."""
|
|
376
|
+
|
|
377
|
+
name: str
|
|
378
|
+
args: list["Expression"]
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@dataclass
|
|
382
|
+
class Variable(ASTNode):
|
|
383
|
+
"""Variable reference: $var."""
|
|
384
|
+
|
|
385
|
+
name: str
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@dataclass
|
|
389
|
+
class Literal(ASTNode):
|
|
390
|
+
"""Literal value: string, number."""
|
|
391
|
+
|
|
392
|
+
value: str | int | float
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@dataclass
|
|
396
|
+
class ForLoop(ASTNode):
|
|
397
|
+
"""For loop: for $var in expr: body."""
|
|
398
|
+
|
|
399
|
+
variable: str
|
|
400
|
+
iterable: "Expression"
|
|
401
|
+
body: list["Statement"]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# Type aliases
|
|
405
|
+
Expression = Union[Pipeline, Command, FunctionCall, Variable, Literal]
|
|
406
|
+
Statement = Union[Assignment, Pipeline, ForLoop, FunctionCall]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class Parser:
|
|
410
|
+
"""Recursive descent parser for Oscura DSL.
|
|
411
|
+
|
|
412
|
+
Parses token stream into abstract syntax tree.
|
|
413
|
+
Supports indentation-based block structure.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
def __init__(self, tokens: list[Token]):
|
|
417
|
+
"""Initialize parser with token list.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
tokens: Token list from lexer
|
|
421
|
+
"""
|
|
422
|
+
self.tokens = tokens
|
|
423
|
+
self.pos = 0
|
|
424
|
+
|
|
425
|
+
def current_token(self) -> Token:
|
|
426
|
+
"""Get current token."""
|
|
427
|
+
if self.pos >= len(self.tokens):
|
|
428
|
+
return self.tokens[-1] # EOF
|
|
429
|
+
return self.tokens[self.pos]
|
|
430
|
+
|
|
431
|
+
def peek_token(self, offset: int = 1) -> Token:
|
|
432
|
+
"""Peek ahead at token."""
|
|
433
|
+
pos = self.pos + offset
|
|
434
|
+
if pos >= len(self.tokens):
|
|
435
|
+
return self.tokens[-1] # EOF
|
|
436
|
+
return self.tokens[pos]
|
|
437
|
+
|
|
438
|
+
def advance(self) -> None:
|
|
439
|
+
"""Advance to next token."""
|
|
440
|
+
if self.pos < len(self.tokens):
|
|
441
|
+
self.pos += 1
|
|
442
|
+
|
|
443
|
+
def expect(self, token_type: TokenType) -> Token:
|
|
444
|
+
"""Expect specific token type and advance.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
token_type: Expected token type
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
The token
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
SyntaxError: If token type doesn't match
|
|
454
|
+
"""
|
|
455
|
+
token = self.current_token()
|
|
456
|
+
if token.type != token_type:
|
|
457
|
+
raise SyntaxError(
|
|
458
|
+
f"Expected {token_type.name}, got {token.type.name} "
|
|
459
|
+
f"at line {token.line}, column {token.column}"
|
|
460
|
+
)
|
|
461
|
+
self.advance()
|
|
462
|
+
return token
|
|
463
|
+
|
|
464
|
+
def skip_newlines(self) -> None:
|
|
465
|
+
"""Skip optional newlines."""
|
|
466
|
+
while self.current_token().type == TokenType.NEWLINE:
|
|
467
|
+
self.advance()
|
|
468
|
+
|
|
469
|
+
def parse(self) -> list[Statement]:
|
|
470
|
+
"""Parse complete program.
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
List of statements (AST)
|
|
474
|
+
|
|
475
|
+
Note:
|
|
476
|
+
May raise SyntaxError on parse errors via parse_statement().
|
|
477
|
+
"""
|
|
478
|
+
statements = []
|
|
479
|
+
|
|
480
|
+
while self.current_token().type != TokenType.EOF:
|
|
481
|
+
self.skip_newlines()
|
|
482
|
+
if self.current_token().type == TokenType.EOF:
|
|
483
|
+
break
|
|
484
|
+
|
|
485
|
+
stmt = self.parse_statement()
|
|
486
|
+
statements.append(stmt)
|
|
487
|
+
self.skip_newlines()
|
|
488
|
+
|
|
489
|
+
return statements
|
|
490
|
+
|
|
491
|
+
def parse_statement(self) -> Statement:
|
|
492
|
+
"""Parse a single statement."""
|
|
493
|
+
# For loop
|
|
494
|
+
if self.current_token().type == TokenType.FOR:
|
|
495
|
+
return self.parse_for_loop()
|
|
496
|
+
|
|
497
|
+
# Assignment or expression
|
|
498
|
+
if self.current_token().type == TokenType.VARIABLE:
|
|
499
|
+
if self.peek_token().type == TokenType.ASSIGN:
|
|
500
|
+
return self.parse_assignment()
|
|
501
|
+
|
|
502
|
+
# Pipeline expression
|
|
503
|
+
return self.parse_pipeline() # type: ignore[return-value]
|
|
504
|
+
|
|
505
|
+
def parse_assignment(self) -> Assignment:
|
|
506
|
+
"""Parse variable assignment."""
|
|
507
|
+
token = self.current_token()
|
|
508
|
+
var_token = self.expect(TokenType.VARIABLE)
|
|
509
|
+
self.expect(TokenType.ASSIGN)
|
|
510
|
+
expr = self.parse_pipeline()
|
|
511
|
+
|
|
512
|
+
return Assignment(
|
|
513
|
+
variable=var_token.value,
|
|
514
|
+
expression=expr,
|
|
515
|
+
line=token.line,
|
|
516
|
+
column=token.column,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
def parse_pipeline(self) -> Expression:
|
|
520
|
+
"""Parse pipeline expression."""
|
|
521
|
+
stages = [self.parse_primary()]
|
|
522
|
+
|
|
523
|
+
while self.current_token().type == TokenType.PIPE:
|
|
524
|
+
self.advance() # Skip |
|
|
525
|
+
stages.append(self.parse_primary())
|
|
526
|
+
|
|
527
|
+
if len(stages) == 1:
|
|
528
|
+
return stages[0]
|
|
529
|
+
|
|
530
|
+
return Pipeline(stages=stages, line=stages[0].line, column=stages[0].column)
|
|
531
|
+
|
|
532
|
+
def parse_primary(self) -> Expression:
|
|
533
|
+
"""Parse primary expression."""
|
|
534
|
+
token = self.current_token()
|
|
535
|
+
|
|
536
|
+
# Literal string
|
|
537
|
+
if token.type == TokenType.STRING:
|
|
538
|
+
self.advance()
|
|
539
|
+
return Literal(value=token.value, line=token.line, column=token.column)
|
|
540
|
+
|
|
541
|
+
# Literal number
|
|
542
|
+
if token.type == TokenType.NUMBER:
|
|
543
|
+
self.advance()
|
|
544
|
+
return Literal(value=token.value, line=token.line, column=token.column)
|
|
545
|
+
|
|
546
|
+
# Variable
|
|
547
|
+
if token.type == TokenType.VARIABLE:
|
|
548
|
+
self.advance()
|
|
549
|
+
return Variable(name=token.value, line=token.line, column=token.column)
|
|
550
|
+
|
|
551
|
+
# Function call or command
|
|
552
|
+
if token.type in (
|
|
553
|
+
TokenType.IDENTIFIER,
|
|
554
|
+
TokenType.LOAD,
|
|
555
|
+
TokenType.FILTER,
|
|
556
|
+
TokenType.MEASURE,
|
|
557
|
+
TokenType.PLOT,
|
|
558
|
+
TokenType.EXPORT,
|
|
559
|
+
TokenType.GLOB,
|
|
560
|
+
):
|
|
561
|
+
name = token.value
|
|
562
|
+
self.advance()
|
|
563
|
+
|
|
564
|
+
# Function call with parens
|
|
565
|
+
if self.current_token().type == TokenType.LPAREN:
|
|
566
|
+
return self.parse_function_call(name, token)
|
|
567
|
+
|
|
568
|
+
# Command with args
|
|
569
|
+
args = []
|
|
570
|
+
while self.current_token().type not in (
|
|
571
|
+
TokenType.PIPE,
|
|
572
|
+
TokenType.NEWLINE,
|
|
573
|
+
TokenType.EOF,
|
|
574
|
+
TokenType.COLON,
|
|
575
|
+
TokenType.INDENT,
|
|
576
|
+
TokenType.DEDENT,
|
|
577
|
+
):
|
|
578
|
+
args.append(self.parse_primary())
|
|
579
|
+
|
|
580
|
+
return Command(name=name, args=args, line=token.line, column=token.column)
|
|
581
|
+
|
|
582
|
+
raise SyntaxError(
|
|
583
|
+
f"Unexpected token {token.type.name} at line {token.line}, column {token.column}"
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
def parse_function_call(self, name: str, token: Token) -> FunctionCall:
|
|
587
|
+
"""Parse function call with parentheses."""
|
|
588
|
+
self.expect(TokenType.LPAREN)
|
|
589
|
+
|
|
590
|
+
args = []
|
|
591
|
+
while self.current_token().type != TokenType.RPAREN:
|
|
592
|
+
args.append(self.parse_primary())
|
|
593
|
+
if self.current_token().type == TokenType.COMMA:
|
|
594
|
+
self.advance()
|
|
595
|
+
|
|
596
|
+
self.expect(TokenType.RPAREN)
|
|
597
|
+
return FunctionCall(name=name, args=args, line=token.line, column=token.column)
|
|
598
|
+
|
|
599
|
+
def parse_for_loop(self) -> ForLoop:
|
|
600
|
+
"""Parse for loop with indented body.
|
|
601
|
+
|
|
602
|
+
Supports both single-line body and multi-line indented blocks:
|
|
603
|
+
|
|
604
|
+
Single line:
|
|
605
|
+
for $f in glob("*.wfm"): load $f
|
|
606
|
+
|
|
607
|
+
Multi-line (indented block):
|
|
608
|
+
for $f in glob("*.wfm"):
|
|
609
|
+
$data = load $f
|
|
610
|
+
measure $data
|
|
611
|
+
plot $data
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
ForLoop AST node.
|
|
615
|
+
"""
|
|
616
|
+
token = self.current_token()
|
|
617
|
+
self.expect(TokenType.FOR)
|
|
618
|
+
|
|
619
|
+
var_token = self.expect(TokenType.VARIABLE)
|
|
620
|
+
self.expect(TokenType.IN)
|
|
621
|
+
|
|
622
|
+
iterable = self.parse_primary()
|
|
623
|
+
self.expect(TokenType.COLON)
|
|
624
|
+
|
|
625
|
+
body: list[Statement] = []
|
|
626
|
+
|
|
627
|
+
# Check if body follows on same line or is indented block
|
|
628
|
+
if self.current_token().type == TokenType.NEWLINE:
|
|
629
|
+
# Multi-line block: expect INDENT, statements, DEDENT
|
|
630
|
+
self.skip_newlines()
|
|
631
|
+
|
|
632
|
+
if self.current_token().type == TokenType.INDENT:
|
|
633
|
+
self.advance() # Consume INDENT
|
|
634
|
+
|
|
635
|
+
# Parse statements until DEDENT
|
|
636
|
+
while self.current_token().type not in (TokenType.DEDENT, TokenType.EOF):
|
|
637
|
+
self.skip_newlines()
|
|
638
|
+
if self.current_token().type in (TokenType.DEDENT, TokenType.EOF):
|
|
639
|
+
break
|
|
640
|
+
stmt = self.parse_statement()
|
|
641
|
+
body.append(stmt)
|
|
642
|
+
self.skip_newlines()
|
|
643
|
+
|
|
644
|
+
# Consume DEDENT if present
|
|
645
|
+
if self.current_token().type == TokenType.DEDENT:
|
|
646
|
+
self.advance()
|
|
647
|
+
else:
|
|
648
|
+
# No INDENT after newline - parse single statement
|
|
649
|
+
body = [self.parse_statement()]
|
|
650
|
+
else:
|
|
651
|
+
# Single-line body (statement on same line as colon)
|
|
652
|
+
body = [self.parse_statement()]
|
|
653
|
+
|
|
654
|
+
return ForLoop(
|
|
655
|
+
variable=var_token.value,
|
|
656
|
+
iterable=iterable,
|
|
657
|
+
body=body,
|
|
658
|
+
line=token.line,
|
|
659
|
+
column=token.column,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def parse_dsl(source: str) -> list[Statement]:
|
|
664
|
+
"""Parse Oscura DSL source code.
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
source: DSL source code
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
Abstract syntax tree (list of statements)
|
|
671
|
+
|
|
672
|
+
Example:
|
|
673
|
+
>>> # Single-line for loop
|
|
674
|
+
>>> ast = parse_dsl('for $f in glob("*.wfm"): load $f')
|
|
675
|
+
|
|
676
|
+
>>> # Multi-line indented block
|
|
677
|
+
>>> ast = parse_dsl('''
|
|
678
|
+
... for $f in glob("*.wfm"):
|
|
679
|
+
... $data = load $f
|
|
680
|
+
... measure $data
|
|
681
|
+
... ''')
|
|
682
|
+
|
|
683
|
+
Note:
|
|
684
|
+
May raise SyntaxError on parse errors via tokenize() or parse().
|
|
685
|
+
"""
|
|
686
|
+
lexer = Lexer(source)
|
|
687
|
+
tokens = lexer.tokenize()
|
|
688
|
+
parser = Parser(tokens)
|
|
689
|
+
return parser.parse()
|