diff --git a/client/src/components/block-inspector.tsx b/client/src/components/block-inspector.tsx index 01167be1..30158e6e 100644 --- a/client/src/components/block-inspector.tsx +++ b/client/src/components/block-inspector.tsx @@ -45,8 +45,8 @@ export function BlockInspector(props: { let blockAnalysis = analyzeBlockPath(props.protocol, props.location, props.mark, props.blockPath, globalContext); - console.log(props.mark); - console.log(blockAnalysis); + // console.log(props.mark); + // console.log(blockAnalysis); let ancestorGroups = blockAnalysis.groups.slice(0, -1); let leafGroup = blockAnalysis.groups.at(-1)!; diff --git a/client/src/process.tsx b/client/src/process.tsx index 4767332b..1c6cfa2c 100644 --- a/client/src/process.tsx +++ b/client/src/process.tsx @@ -53,7 +53,7 @@ const computeGraph: ProtocolBlockGraphRenderer extends ProtocolBlock { export interface ProcessLocation extends MasterBlockLocation { children: {}; - date: number; mode: { type: 'collecting'; } | { @@ -106,7 +105,10 @@ export interface ProcessLocation extends MasterBlockLocation { } | { type: 'running'; pausable: boolean; - processLocation: Location | null; + processInfo: { + date: number; + location: Location; + } | null; form: 'halting' | 'jumping' | 'normal' | 'paused' | 'pausing'; }; } @@ -149,13 +151,13 @@ export function createProcessBlockImpl(options: { let Component = options.Component; - if (Component && (mode.type === 'running') && mode.processLocation) { + if (Component && (mode.type === 'running') && mode.processInfo) { return ( ); } @@ -197,7 +199,7 @@ export function createProcessBlockImpl(options: { }, createFeatures(block, location) { let processLocation = (location?.mode.type === 'running') - ? location.mode.processLocation + ? location.mode.processInfo?.location ?? null : null; return options.createFeatures?.(block.data, processLocation) ?? [ diff --git a/client/src/protocol.ts b/client/src/protocol.ts index 1219e93b..a1b6edb8 100644 --- a/client/src/protocol.ts +++ b/client/src/protocol.ts @@ -58,7 +58,7 @@ export function getCommonBlockPathLength(a: ProtocolBlockPath, b: ProtocolBlockP return index; } -export function getRefPaths(block: ProtocolBlock, location: unknown, context: GlobalContext): ProtocolBlockPath[] { +export function getRefPaths(block: ProtocolBlock, location: MasterBlockLocation, context: GlobalContext): ProtocolBlockPath[] { let blockImpl = getBlockImpl(block, context); let children = blockImpl.getChildren?.(block, context); diff --git a/host/pr1/__init__.py b/host/pr1/__init__.py index 4a2f2931..b29c0542 100644 --- a/host/pr1/__init__.py +++ b/host/pr1/__init__.py @@ -19,6 +19,8 @@ from .langservice import * from .master.analysis import * from .plugin.manager import * +from .procedure import * +from .rich_text import * from .staticanalysis.expr import * from .staticanalysis.expression import * from .staticanalysis.support import * diff --git a/host/pr1/devices/nodes/value.py b/host/pr1/devices/nodes/value.py index 464777e9..7576db56 100644 --- a/host/pr1/devices/nodes/value.py +++ b/host/pr1/devices/nodes/value.py @@ -181,7 +181,7 @@ def export(self): "writable": self.writable } - def export_value(self, value: Optional[T | NullType], /): + def export_value(self, value: Optional[T | NullType], /) -> object: match value: case None: return None diff --git a/host/pr1/draft.py b/host/pr1/draft.py index 192fc16d..72602739 100644 --- a/host/pr1/draft.py +++ b/host/pr1/draft.py @@ -7,7 +7,7 @@ from .document import Document if TYPE_CHECKING: - from .fiber.parser import FiberProtocol + from .fiber.parser import FiberProtocol, GlobalContext from .input import LanguageServiceAnalysis from .host import Host @@ -64,7 +64,7 @@ class DraftCompilation: draft_id: str protocol: 'Optional[FiberProtocol]' - def export(self): + def export(self, context: 'GlobalContext'): return { "analysis": { "completions": [completion.export() for completion in self.analysis.completions], @@ -79,6 +79,6 @@ def export(self): "warnings": [warning.export() for warning in self.analysis.warnings] }, "missingDocumentPaths": [], # str(path).split("/") for path in self.document_paths], - "protocol": self.protocol and self.protocol.export(), + "protocol": self.protocol and self.protocol.export(context), "valid": (not self.analysis.errors) } diff --git a/host/pr1/fiber/expr.py b/host/pr1/fiber/expr.py index 6bb0823f..4049ba63 100644 --- a/host/pr1/fiber/expr.py +++ b/host/pr1/fiber/expr.py @@ -7,7 +7,7 @@ from dataclasses import KW_ONLY, dataclass from enum import Enum from types import EllipsisType, NoneType -from typing import Any, Generic, Literal, TypeVar, cast, overload +from typing import Any, Callable, Generic, Literal, TypeVar, cast, overload from quantops import Quantity @@ -238,6 +238,24 @@ def eval(self, context: EvalContext, *, final: bool): def export(self): raise NotImplementedError + def export_inner(self, export_inner_value: Callable[[Any], Any], /): + from ..input import EvaluableChain + from ..input.dynamic import EvaluableDynamicValue + + match self: + case EvaluableConstantValue(inner_value): + return { + "type": "constant", + "innerValue": export_inner_value(inner_value.value) + } + case EvaluablePythonExpr(contents) | EvaluableChain(EvaluablePythonExpr(contents)) | EvaluableDynamicValue(EvaluablePythonExpr(contents)): + return { + "type": "expression", + "contents": contents.value + } + case _: + raise ValueError + @dataclass class PythonExprObject: diff --git a/host/pr1/fiber/master2.py b/host/pr1/fiber/master2.py index 042a52a3..6b9e31c0 100644 --- a/host/pr1/fiber/master2.py +++ b/host/pr1/fiber/master2.py @@ -1,5 +1,4 @@ from asyncio import Task -import math from random import random import time from uuid import uuid4 @@ -8,7 +7,6 @@ from dataclasses import dataclass, field from os import PathLike from pathlib import Path -from pprint import pprint from traceback import StackSummary from typing import IO, TYPE_CHECKING, Any, Optional, Self import asyncio @@ -21,14 +19,14 @@ from ..util.asyncio import wait_all from ..host import logger from ..util.decorators import provide_logger -from ..history import TreeAdditionChange, BaseTreeChange, TreeChange, TreeRemovalChange, TreeUpdateChange +from ..history import TreeAdditionChange, TreeChange, TreeRemovalChange, TreeUpdateChange from ..util.pool import Pool from ..util.types import SimpleCallbackFunction from ..util.misc import Exportable, HierarchyNode, IndexCounter -from ..master.analysis import MasterAnalysis, RuntimeAnalysis, RuntimeMasterAnalysisItem +from ..master.analysis import MasterAnalysis, RuntimeAnalysis from .process import ProgramExecEvent from .eval import EvalContext, EvalStack -from .parser import BaseBlock, BaseProgramPoint, BaseProgram, FiberProtocol, HeadProgram +from .parser import BaseBlock, BaseProgramPoint, BaseProgram, GlobalContext, HeadProgram from ..experiment import Experiment from ..ureg import ureg @@ -59,6 +57,7 @@ def __init__(self, compilation: DraftCompilation, /, experiment: Experiment, *, self._entry_counter = IndexCounter(start=1) self._events = list[ProgramExecEvent]() self._file: IO[bytes] + self._location: Any self._logger: Logger self._next_analysis_item_id = 0 self._owner: ProgramOwner @@ -155,6 +154,8 @@ async def func(): self.experiment.save() def receive(self, exec_path: list[int], message: Any): + self._logger.critical(f"Received {message!r}") + current_handle = self._handle for exec_key in exec_path: @@ -166,16 +167,16 @@ def study_block(self, block: BaseBlock): return self._owner.study_block(block) - def export(self): + def export(self) -> object: if not self._root_entry: return None return { "id": self.id, "initialAnalysis": self._initial_analysis.export(), - "location": self._root_entry.export(), + "location": self._location, "masterAnalysis": self._master_analysis.export(), - "protocol": self.protocol.export(), + "protocol": self.protocol.export(GlobalContext(self.host)), "startDate": (self.start_time * 1000) } @@ -268,7 +269,12 @@ def update_handle(handle: ProgramHandle, parent_entry: Optional[ProgramHandleEnt update_handle(self._handle) self._master_analysis += analysis + self._logger.debug(f"Update: {user_significant}") + if self._update_callback and user_significant: + assert self._root_entry + self._location = self._root_entry.export() + self._update_callback() event = ExperimentReportEvent( diff --git a/host/pr1/fiber/parser.py b/host/pr1/fiber/parser.py index 350a0e7a..b1a14799 100644 --- a/host/pr1/fiber/parser.py +++ b/host/pr1/fiber/parser.py @@ -195,6 +195,10 @@ def stable(self): class BaseProgramPoint(ExportableABC): pass +@dataclass(frozen=True, slots=True) +class GlobalContext: + host: 'Host' + class BaseBlock(ABC, HierarchyNode): def duration(self): return DurationTerm.unknown() @@ -208,7 +212,7 @@ def import_point(self, data: Any, /) -> BaseProgramPoint: ... @abstractmethod - def export(self): + def export(self, context: GlobalContext) -> object: ... # @deprecated @@ -481,18 +485,18 @@ def update(self, **kwargs): @dataclass(kw_only=True) -class FiberProtocol(Exportable): +class FiberProtocol: # (Exportable): details: ProtocolDetails draft: Draft global_symbol: EvalSymbol name: Optional[str] root: BaseBlock - def export(self): + def export(self, context: GlobalContext) -> object: return { "draft": self.draft.export(), "name": self.name, - "root": self.root.export() + "root": self.root.export(context) } diff --git a/host/pr1/host.py b/host/pr1/host.py index 01f8821e..da99f26c 100644 --- a/host/pr1/host.py +++ b/host/pr1/host.py @@ -15,7 +15,7 @@ from .draft import Draft, DraftCompilation from .experiment import Experiment, ExperimentId from .fiber.master2 import Master -from .fiber.parser import AnalysisContext +from .fiber.parser import AnalysisContext, GlobalContext from .input import (Attribute, BoolType, KVDictType, PrimitiveType, RecordType, StrType, UnionType) from .langservice import LanguageServiceAnalysis @@ -52,6 +52,14 @@ def find(self, path: NodePath) -> Optional[BaseNode]: return node + # def find_unchecked(self, path: NodePath) -> BaseNode: + # node = self.find(path) + + # if not node: + # raise ValueError(f"Node with path {path!r} not found") + + # return node + class PluginConf(Protocol): development: bool @@ -305,7 +313,7 @@ async def process_request(self, request, *, agent) -> Any: study = None return { - **compilation.export(), + **compilation.export(GlobalContext(self)), "study": study and { "mark": study[1].export(), "point": study[0].export() diff --git a/host/pr1/procedure.py b/host/pr1/procedure.py index 9ef42274..43082253 100644 --- a/host/pr1/procedure.py +++ b/host/pr1/procedure.py @@ -48,7 +48,7 @@ def create_program(self, handle): def import_point(self, data, /): return self._process.import_point(data) - def export(self): + def export(self, context): return { "name": self._process.name, "namespace": self._process.namespace, @@ -168,7 +168,7 @@ def send_warning(self, warning: Diagnostic, /): self._program._handle.send_analysis(RuntimeAnalysis(warnings=[warning])) def send_location(self, location: T_ProcessLocation, /): - self._mode.process_location = location + self._mode.process_info = (time.time(), location) self._program._send_location() @@ -309,7 +309,7 @@ def export(self): @dataclass(slots=True) class Running: form: ProcessProgramForm.Any - process_location: Optional[Exportable] + process_info: Optional[tuple[float, Exportable]] pausable: bool task: Task[None] = field(repr=False) term: Term @@ -317,7 +317,7 @@ class Running: def location(self): return ProcessProgramMode.RunningLocation( form=self.form.location, - process_location=self.process_location, + process_info=self.process_info, pausable=self.pausable ) @@ -325,14 +325,17 @@ def location(self): @dataclass(frozen=True, slots=True) class RunningLocation: form: ProcessProgramFormLocation - process_location: Optional[Exportable] + process_info: Optional[tuple[float, Exportable]] pausable: bool def export(self): return { "type": "running", "form": self.form.export(), - "processLocation": self.process_location and self.process_location.export(), + "processInfo": self.process_info and { + "date": (self.process_info[0] * 1000), + "location": self.process_info[1].export() + }, "pausable": self.pausable } @@ -347,7 +350,6 @@ class ProcessProgramLocation: def export(self): return { - "date": time.time() * 1000, "mode": self.mode.export() } @@ -489,7 +491,7 @@ async def run(self, point: Optional[ProcessProgramPoint], stack): self._mode = ProcessProgramMode.Running( form=ProcessProgramForm.Normal(), - process_location=None, + process_info=None, pausable=False, task=asyncio.create_task(self._block._process(self._context)), term=DurationTerm.unknown() diff --git a/host/pr1/reader.py b/host/pr1/reader.py index 157d40d6..b125ced8 100644 --- a/host/pr1/reader.py +++ b/host/pr1/reader.py @@ -781,7 +781,7 @@ class StackEntry: class DuplicateKeyError(ReaderError): def __init__(self, original: LocatedString, duplicate: LocatedString, /): super().__init__( - "Invalid value, expected expression", + "Duplicate key", references=[ DiagnosticDocumentReference.from_value(original, id='origin'), DiagnosticDocumentReference.from_value(duplicate, id='duplicate') diff --git a/host/pr1/util/misc.py b/host/pr1/util/misc.py index 9fea77db..4ed2057d 100644 --- a/host/pr1/util/misc.py +++ b/host/pr1/util/misc.py @@ -58,12 +58,12 @@ def split_sequence(sequence: Sequence[T], func: SequenceSplitter[T], /): @typing.runtime_checkable class Exportable(Protocol): - def export(self) -> Any: + def export(self) -> object: ... class ExportableABC(ABC): @abstractmethod - def export(self) -> Any: + def export(self) -> object: ... class UnreachableError(Exception): diff --git a/units/core/client/src/devices/blocks/publisher.ts b/units/core/client/src/devices/blocks/publisher.ts index 01bfa8e7..7118d265 100644 --- a/units/core/client/src/devices/blocks/publisher.ts +++ b/units/core/client/src/devices/blocks/publisher.ts @@ -1,12 +1,13 @@ -import { DynamicValue, PluginBlockImpl, formatDynamicValue, util } from 'pr1'; +import { DynamicValue, EvaluableValue, PluginBlockImpl, formatEvaluable, ureg } from 'pr1'; import { MasterBlockLocation, ProtocolBlock, createZeroTerm } from 'pr1-shared'; -import { ExecutorState, NodePath, ValueNode, namespace } from '../types'; +import { ReactNode, createElement } from 'react'; +import { EnumValue, ExecutorState, NodePath, NullableValue, NumericValue, ValueNode, namespace } from '../types'; import { findNode } from '../util'; export interface PublisherBlock extends ProtocolBlock { - assignments: [NodePath, DynamicValue][]; + assignments: [NodePath, EvaluableValue][]; child: ProtocolBlock; } @@ -42,12 +43,36 @@ export default { return block.assignments.map(([path, value]) => { let parentNode = findNode(executor.root, path.slice(0, -1))!; let node = findNode(executor.root, path) as ValueNode; - let locationValue = location?.assignments.find(([otherPath, value]) => (util.deepEqual(otherPath, path)))?.[1]; + // let locationValue = location?.assignments.find(([otherPath, value]) => (util.deepEqual(otherPath, path)))?.[1]; + + let label: ReactNode = formatEvaluable(value, (nullableValue) => { + if (nullableValue.type === 'null') { + return '[Disabled]'; + } + + switch (node.spec.type) { + case 'boolean': { + return nullableValue.innerValue ? 'On' : 'Off'; + } + + case 'enum': { + let enumValue = nullableValue.innerValue as EnumValue; + let enumCase = node.spec.cases.find((enumCase) => (enumCase.id === enumValue))!; + + return enumCase.label ?? enumCase.id; + } + + case 'numeric': { + let numericValue = nullableValue.innerValue as NumericValue; + return ureg.formatQuantityAsReact(numericValue.magnitude, (node.spec.resolution ?? 0), ureg.deserializeContext(node.spec.context), { createElement }); + }; + } + }); return { description: `${parentNode.label ?? parentNode.id} › ${node.label ?? node.id}`, icon: (node.icon ?? 'settings_input_hdmi'), - label: formatDynamicValue(locationValue ?? value) + label }; }); }, diff --git a/units/core/client/src/devices/components/node-hierarchy.tsx b/units/core/client/src/devices/components/node-hierarchy.tsx index 875e5704..e3e75c95 100644 --- a/units/core/client/src/devices/components/node-hierarchy.tsx +++ b/units/core/client/src/devices/components/node-hierarchy.tsx @@ -4,7 +4,7 @@ import { ReactNode, createElement, useState } from 'react'; import styles from './node-hierarchy.module.scss'; -import { BaseNode, CollectionNode, Context, NodePath, NodePreference, NodeStates, NumericValue } from '../types'; +import { BaseNode, CollectionNode, Context, EnumValue, NodePath, NodePreference, NodeStates, NumericValue } from '../types'; import { isCollectionNode, isValueNode, iterNodes } from '../util'; @@ -180,7 +180,7 @@ export function NodeHierarchyNode(props: { } case 'enum': { - let caseId = lastValue.innerValue as (number | string); + let caseId = lastValue.innerValue as EnumValue; let specCase = spec.cases.find((specCase) => (specCase.id === caseId))!; entryValue = (specCase.label ?? specCase.id); diff --git a/units/core/client/src/devices/types.ts b/units/core/client/src/devices/types.ts index 66f06c56..37706ee4 100644 --- a/units/core/client/src/devices/types.ts +++ b/units/core/client/src/devices/types.ts @@ -104,14 +104,18 @@ export type NodeStates = ImMap; export interface ValueEvent { time: number; - value: { - type: 'null'; - } | { - type: 'default'; - innerValue: unknown; - } | null; + value: NullableValue | null; } +export type NullableValue = { + type: 'null'; +} | { + type: 'default'; + innerValue: T; +}; + +export type EnumValue = number | string; + export interface NumericValue { magnitude: number; } diff --git a/units/core/src/pr1_devices/parser.py b/units/core/src/pr1_devices/parser.py index c1a7056a..fc3c0be2 100644 --- a/units/core/src/pr1_devices/parser.py +++ b/units/core/src/pr1_devices/parser.py @@ -2,14 +2,14 @@ import functools from dataclasses import dataclass from types import EllipsisType -from typing import TYPE_CHECKING, Any, final +from typing import TYPE_CHECKING, Any, cast, final -import pr1 as am +import automancer as am from pr1.devices.nodes.collection import CollectionNode from pr1.devices.nodes.common import BaseNode, NodePath from pr1.devices.nodes.numeric import NumericNode from pr1.devices.nodes.primitive import BooleanNode, EnumNode -from pr1.devices.nodes.value import ValueNode +from pr1.devices.nodes.value import Null, ValueNode from pr1.fiber.eval import EvalContext, EvalEnv, EvalEnvValue, EvalSymbol from pr1.fiber.expr import Evaluable, export_value from pr1.fiber.parser import (BaseBlock, BaseParser, @@ -17,8 +17,8 @@ BasePassiveTransformer, BlockUnitState, FiberParser, ProtocolUnitData, ProtocolUnitDetails, TransformerAdoptionResult) -from pr1.input import (AnyType, Attribute, AutoExprContextType, BoolType, - EnumType, PrimitiveType, QuantityType) +from pr1.input import (AnyType, Attribute, AutoExprContextType, BoolType, ChainType, + EnumType, PrimitiveType, QuantityType, Type) from pr1.reader import LocatedValue from pr1.util.decorators import debug @@ -59,12 +59,12 @@ def create_program(self, handle): def import_point(self, data, /): return self.child.import_point(data) - def export(self): + def export(self, context): return { "namespace": namespace, "name": "applier", - "child": self.child.export(), + "child": self.child.export(context), "duration": self.duration().export() } @@ -92,18 +92,23 @@ def create_program(self, handle): def import_point(self, data, /): return self.child.import_point(data) - def export(self): + def export(self, context) -> object: return { "namespace": namespace, "name": "publisher", - "assignments": [[path, export_value(value)] for path, value in self.assignments.items()], + "assignments": [[path, value.export_inner(cast(ValueNode, context.host.root_node.find(path)).export_value)] for path, value in self.assignments.items()], "duration": self.duration().export(), - "child": self.child.export(), + "child": self.child.export(context), "stable": self.stable } +class NoneToNullType(Type): + def analyze(self, obj, /, context): + result = LocatedValue(am.Null, obj.area) if obj.value is None else obj + return am.DiagnosticAnalysis(), am.EvaluableConstantValue(result) if context.auto_expr else result + class PublisherTransformer(BasePassiveTransformer): priority = 100 @@ -119,7 +124,10 @@ def get_type(node): case EnumNode(): return EnumType(*[case.id for case in node.cases]) case NumericNode(): - return QuantityType(node.context.dimensionality, allow_nil=node.nullable, min=(node.range[0] if node.range else None), max=(node.range[1] if node.range else None)) + return ChainType( + QuantityType(node.context.dimensionality, allow_nil=node.nullable, min=(node.range[0] if node.range else None), max=(node.range[1] if node.range else None)), + NoneToNullType() + ) case _: return AnyType() diff --git a/units/core/src/pr1_repeat/parser.py b/units/core/src/pr1_repeat/parser.py index 500102c6..eb83f721 100644 --- a/units/core/src/pr1_repeat/parser.py +++ b/units/core/src/pr1_repeat/parser.py @@ -96,11 +96,11 @@ def import_point(self, data, /): iteration=data["iteration"] ) - def export(self): + def export(self, context) -> object: return { "name": "_", "namespace": namespace, "count": self.count.export(), - "child": self.block.export(), + "child": self.block.export(context), "duration": self.duration().export() } diff --git a/units/core/src/pr1_sequence/parser.py b/units/core/src/pr1_sequence/parser.py index 7ce2ef79..437685ee 100644 --- a/units/core/src/pr1_sequence/parser.py +++ b/units/core/src/pr1_sequence/parser.py @@ -94,11 +94,11 @@ def import_point(self, data, /): index=index ) - def export(self): + def export(self, context): return { "name": "_", "namespace": namespace, - "children": [child.export() for child in self.children], + "children": [child.export(context) for child in self.children], "childrenDelays": [am.DurationTerm.zero().export(), *[delay.export() for delay in cumsum([child.duration() for child in self.children])]][:-1], "duration": self.duration().export() }