import type { Asset } from '../model/Asset'; import type { PropertyBag, PropertyValue } from '../model/Properties'; import type { Project } from '../model/Project'; import { isGlobal } from '../model/Scope'; import type { PropertyDef } from '../properties/PropertySchema'; import type { NodeContext, SharedState } from './NodeContext'; export function emptySharedState(): SharedState { return { variables: {}, states: {} }; } /** * Built once per run, from the declarations rather than from any instance: a global variable exists * even before the behavior declaring it is placed. Two behaviors declaring the same name mean the * same variable – the first declaration sets the starting value (see `globalConflicts`). */ export function buildSharedState(project: Project): SharedState { const shared = emptySharedState(); for (const asset of project.assets) { if (asset.type === 'behavior') { for (const def of asset.variables) if (isGlobal(def) && !(def.key in shared.variables)) shared.variables[def.key] = asset.props[def.key] ?? def.default; } else if (asset.type === 'statemachine' && isGlobal(asset)) { shared.states[asset.id] = ''; } } return shared; } /** * Where two behaviors disagree about the same global name. A different kind is an error – one of the * two would silently read a value of a type it never declared – while a different starting value is * only a warning, since the first declaration decides and the run is well defined either way. */ export interface GlobalConflict { key: string; severity: 'error' | 'warning'; message: string; } export function globalConflicts(project: Project): GlobalConflict[] { const first = new Map(); const conflicts: GlobalConflict[] = []; const seen = new Set(); for (const asset of project.assets) { if (asset.type !== 'behavior') continue; for (const def of asset.variables) { if (!isGlobal(def)) continue; const value = asset.props[def.key] ?? def.default; const held = first.get(def.key); if (!held) { first.set(def.key, { def, asset: asset.name, value }); continue; } // One report per name: a third behavior disagreeing adds nothing the first line does not say. if (seen.has(def.key)) continue; if (held.def.kind !== def.kind) { seen.add(def.key); conflicts.push({ key: def.key, severity: 'error', message: `Global variable "${def.key}" is a ${held.def.kind} in ${held.asset} and a ${def.kind} in ${asset.name}.`, }); } else if (!sameValue(held.value, value)) { seen.add(def.key); conflicts.push({ key: def.key, severity: 'warning', message: `Global variable "${def.key}" starts at ${display(held.value)} in ${held.asset} and at ${display(value)} in ${asset.name}; the first is used.`, }); } } } return conflicts; } /** Structs and lists compare by their stored shape, which is what the project file holds anyway. */ function sameValue(a: PropertyValue | undefined, b: PropertyValue | undefined): boolean { return a === b || JSON.stringify(a ?? null) === JSON.stringify(b ?? null); } function display(value: PropertyValue | undefined): string { return typeof value === 'string' ? `"${value}"` : JSON.stringify(value ?? null); } /** The bag a variable is written to: its own when the behavior declares it, else the shared one. */ export function variableBag(ctx: NodeContext, key: string): PropertyBag { if (key in ctx.variables) return ctx.variables; return key in ctx.shared.variables ? ctx.shared.variables : ctx.variables; } /** Everything the behavior can read under one name, its own declaration winning over a shared one. */ export function readableVariables(ctx: NodeContext): PropertyBag { const shared = ctx.shared.variables; return isEmpty(shared) ? ctx.variables : { ...shared, ...ctx.variables }; } /** Where a machine's current state is kept: shared when the machine is `global`, else per instance. */ export function stateBag(ctx: NodeContext, machineId: string): Record { return machineId in ctx.shared.states ? ctx.shared.states : ctx.states; } /** The defaults a behavior instance starts with: a shared variable is not one of them. */ export function withoutGlobals(defaults: PropertyBag, asset: Asset): PropertyBag { if (asset.type !== 'behavior') return defaults; const own = { ...defaults }; for (const def of asset.variables) if (isGlobal(def)) delete own[def.key]; return own; } function isEmpty(bag: PropertyBag): boolean { for (const key in bag) if (Object.hasOwn(bag, key)) return false; return true; }