Private/Analysis.ps1
|
# The analysis step of a load, between reading files and merging them. It finds what can't be known up front: each # !if and each path with a reference. It decides them in an order where each one sees everything that can change what # it reads. Files behind a true !if are loaded as it goes. Files behind a false !if are never read, because reading a # .ps1 through -Loader runs it. # The '!merge' or '!include' entries of a file that count now: what it wrote, changed by its operators, then by # the operators of the true !if in it. Returns @{ Entry; Via }. Via has the ids of the !if an entry came from, or is $null. function Get-PsdActiveEntries { param($File, [string] $Kind, $Doc) $out = New-PsdList foreach ($e in $File.$Kind) { $out.Add(@{ Entry = $e; Via = $null }) } foreach ($op in $File.DirOps) { if ($op.Kind -eq $Kind) { Invoke-PsdEntryOp $op $Doc $null $out } } if ($null -ne $File.Root) { Add-PsdIfEntries $File.Root $Kind $Doc @() $out } , $out } function Add-PsdIfEntries { param($Map, [string] $Kind, $Doc, [object[]] $Via, $Out) foreach ($if in $Map.Ifs) { if (-not $Doc.Decisions[$if.Id]) { continue } $owners = @($Via) + $if.Id foreach ($op in $if.Body.DirOps) { if ($op.Kind -eq $Kind) { Invoke-PsdEntryOp $op $Doc $owners $Out } } Add-PsdIfEntries $if.Body $Kind $Doc $owners $Out } } # True when the entry has this path. A path with a reference that isn't decided yet equals nothing. function Test-PsdEntryPath { param($Item, [string] $Path, $Doc) $own = Get-PsdEntryPath $Item.Entry $Doc $null -ne $own -and [string]::Equals($own, $Path, [StringComparison]::OrdinalIgnoreCase) } # Runs one operator on the entries, with the rules of the list operators. A missing item is an error only once # everything is decided: before that, an !if that adds it may still be open. function Invoke-PsdEntryOp { param($Op, $Doc, $Via, $Out) $new = @(foreach ($e in $Op.Items) { @{ Entry = $e; Via = $Via } }) $missing = { param($what) if ($Doc.Settled) { Add-PsdKeyError $Doc.Ctx 'PSD212' "$what isn't in $($Op.Name), so it can't be changed." $Op.Info } } if ($null -ne $Op.Match) { $hit = $false for ($j = $Out.Count - 1; $j -ge 0; $j--) { if (Test-PsdEntryPath $Out[$j] $Op.Match $Doc) { $hit = $true $Out.RemoveAt($j) if ($Op.Op -eq ':=') { $Out.InsertRange($j, [object[]] $new) } } } if (-not $hit) { & $missing "'$($Op.Match)'" } return } if ($null -ne $Op.Index) { $i = $Op.Index $count = $Out.Count if ($Op.Op -eq '+=') { if ($i -lt 0) { $i = $count + 1 + $i } $Out.InsertRange([Math]::Max(0, [Math]::Min($i, $count)), [object[]] $new) return } if ($i -lt 0) { $i += $count } if ($i -lt 0 -or $i -ge $count) { & $missing "[$($Op.Index)]"; return } $Out.RemoveAt($i) if ($Op.Op -eq ':=') { $Out.InsertRange($i, [object[]] $new) } return } switch ($Op.Op) { ':=' { $Out.Clear(); $Out.AddRange([object[]] $new) } '+=' { $Out.AddRange([object[]] $new) } '=+' { $Out.InsertRange(0, [object[]] $new) } '|=' { foreach ($n in $new) { $path = Get-PsdEntryPath $n.Entry $Doc $have = $false foreach ($x in $Out) { if ($null -ne $path -and (Test-PsdEntryPath $x $path $Doc)) { $have = $true; break } } if (-not $have) { $Out.Add($n) } } } '-=' { if ($Op.All) { $Out.Clear(); return } foreach ($n in $new) { $path = Get-PsdEntryPath $n.Entry $Doc if ($null -eq $path) { continue } $hit = $false for ($j = $Out.Count - 1; $j -ge 0; $j--) { if (Test-PsdEntryPath $Out[$j] $path $Doc) { $hit = $true; $Out.RemoveAt($j) } } if (-not $hit) { & $missing "'$path'" } } } } } # The path of an entry, or $null while a reference in it isn't decided. function Get-PsdEntryPath { param($Entry, $Doc) if ($null -eq $Entry.Ref) { return $Entry.Path } $Doc.Paths[$Entry.Ref.Id] } # Follows the files of the document again: its layers, the names of its files and its includes. function Update-PsdDocFiles { param($Doc, $Sources) $ctx = $Doc.Ctx $layers = New-PsdList foreach ($s in $Sources) { if ($s.ContainsKey('Layer')) { $layers.Add($s.Layer) } else { foreach ($l in (Get-PsdLayers $s.File $ctx $Doc)) { $layers.Add($l) } } } $Doc.Layers = $layers $Doc.AllTree = $null foreach ($l in $layers) { $f = $l.File if (-not $f.Synthetic -or $f.Named) { Register-PsdNs $Doc $Doc.Ns $f.Ns $f $f $null $ctx } } foreach ($l in $layers) { foreach ($a in (Get-PsdActiveEntries $l.File 'Include' $Doc)) { $path = Get-PsdEntryPath $a.Entry $Doc if (-not $path) { continue } foreach ($p in (Resolve-PsdPaths $path $l.File $a.Entry.Node $ctx)) { $f = Read-PsdFile $p $ctx $a.Entry.Node 'PSD200' if ($null -eq $f -or $null -eq $f.Root) { continue } $idoc = Get-PsdIncludedDoc $f $ctx $a.Entry.Node if ($null -ne $idoc) { Register-PsdNs $Doc $Doc.Inc $f.Ns $f $idoc $a.Entry.Node $ctx } } } } } # The document as far as it is decided: true !if are merged, the others are left out. function New-PsdPartialDoc { param($Doc) $pre = New-PsdDocObject $Doc.Entry $Doc.Layers $Doc.Ctx $pre.Ns = $Doc.Ns $pre.Inc = $Doc.Inc $pre.Rules = $Doc.Rules $pre.Defaults = $Doc.Defaults $pre.Decisions = $Doc.Decisions $pre.Paths = $Doc.Paths $pre.Final = $Doc $pre.Tree = Merge-PsdLayers $pre $true $pre } function New-PsdOpenItem { param([string] $Kind, $Id, $Node) @{ Kind = $Kind; Id = $Id; Node = $Node; Reads = @{}; Writes = @{}; Loads = $true; Result = $null; Round = 0; Partial = $null } } # True when the body of an !if loads a file, so what it writes can't be known yet. function Test-PsdMapLoads { param($Map) foreach ($op in $Map.DirOps) { if ($op.Kind -eq 'Merge') { return $true } } foreach ($if in $Map.Ifs) { if (Test-PsdMapLoads $if.Body) { return $true } } $false } function Add-PsdOpenInNode { param($Node, $Doc, $Out, $Seen) if ($Node.K -eq 'M') { Add-PsdOpenIfs $Node $Doc $Out $Seen } elseif ($Node.K -eq 'L') { foreach ($x in $Node.V) { Add-PsdOpenInNode $x $Doc $Out $Seen } } } function Add-PsdOpenIfs { param($Map, $Doc, $Out, $Seen) foreach ($k in $Map.V.PSBase.Keys) { Add-PsdOpenInNode $Map.V[$k] $Doc $Out $Seen } foreach ($op in $Map.Ops) { Add-PsdOpenInNode $op.Arg $Doc $Out $Seen } foreach ($if in $Map.Ifs) { if ($Doc.Decisions.ContainsKey($if.Id)) { if ($Doc.Decisions[$if.Id]) { Add-PsdOpenIfs $if.Body $Doc $Out $Seen } } elseif (-not $Seen.ContainsKey($if.Id)) { $Seen[$if.Id] = $true $Out.Add((New-PsdOpenItem 'If' $if.Id $if)) } } } # What is still to be decided in the files loaded so far. function Get-PsdOpenItems { param($Doc) $out = New-PsdList $seen = @{} foreach ($l in $Doc.Layers) { $root = Get-PsdLayerRoot $l if ($null -ne $root) { Add-PsdOpenIfs $root $Doc $out $seen } foreach ($kind in 'Merge', 'Include') { foreach ($a in (Get-PsdActiveEntries $l.File $kind $Doc)) { $ref = $a.Entry.Ref if ($null -ne $ref -and -not $Doc.Paths.ContainsKey($ref.Id) -and -not $seen.ContainsKey($ref.Id)) { $seen[$ref.Id] = $true $out.Add((New-PsdOpenItem 'Path' $ref.Id $ref)) } } } } , $out } # Adds the key paths an expression reads to Reads, and what the values there read in turn. name:: and env: are left # out: no decision here can change them. function Add-PsdExprReads { param($Expr, $Tree, $Reads, $Seen) foreach ($op in $Expr.Ops) { if ($op.T -ne 'Path' -or $op.Env -or $op.Ns -or $op.Builtin) { continue } $keys = New-PsdList foreach ($s in $op.Segs) { if ($s.IsIndex) { break } $keys.Add($s.Key) } if ($keys.Count -eq 0) { continue } $Reads[($keys -join '.').ToLowerInvariant()] = $op.Segs Add-PsdNodeReads (Get-PsdTreeNode $Tree $op.Segs) $Tree $Reads $Seen } foreach ($f in $Expr.Filters) { foreach ($a in $f.Args) { Add-PsdExprReads $a $Tree $Reads $Seen } } } function Add-PsdNodeReads { param($Node, $Tree, $Reads, $Seen) if ($null -eq $Node -or $Seen.ContainsKey($Node.Id)) { return } $Seen[$Node.Id] = $true switch ($Node.K) { 'T' { foreach ($p in $Node.Parts) { if ($p -isnot [string]) { Add-PsdExprReads $p.Expr $Tree $Reads $Seen } } } 'O' { Add-PsdNodeReads $Node.Base $Tree $Reads $Seen; Add-PsdNodeReads $Node.Arg $Tree $Reads $Seen } 'M' { foreach ($k in $Node.V.PSBase.Keys) { Add-PsdNodeReads $Node.V[$k] $Tree $Reads $Seen } } 'L' { foreach ($x in $Node.V) { Add-PsdNodeReads $x $Tree $Reads $Seen } } } } function Add-PsdCondReads { param($Cond, $Tree, $Reads, $Seen) switch ($Cond.T) { { $_ -in 'Or', 'And' } { Add-PsdCondReads $Cond.L $Tree $Reads $Seen; Add-PsdCondReads $Cond.R $Tree $Reads $Seen } 'Not' { Add-PsdCondReads $Cond.X $Tree $Reads $Seen } 'Truth' { if ($Cond.X.T -ne 'Lit') { Add-PsdExprReads $Cond.X.Expr $Tree $Reads $Seen } } 'Cmp' { foreach ($x in $Cond.L, $Cond.R) { if ($x.T -ne 'Lit') { Add-PsdExprReads $x.Expr $Tree $Reads $Seen } } } } } # Adds the key paths a map writes to Out, with the !if in it. function Add-PsdMapWrites { param($Map, [string] $Prefix, $Out) foreach ($k in $Map.V.PSBase.Keys) { $path = "$Prefix$k".ToLowerInvariant() $v = $Map.V[$k] if ($v.K -eq 'M' -and ($v.V.PSBase.Count -or $v.Ops.Count -or $v.Ifs.Count)) { Add-PsdMapWrites $v "$path." $Out } else { $Out[$path] = $true } } foreach ($op in $Map.Ops) { $Out["$Prefix$($op.Key)".ToLowerInvariant()] = $true } foreach ($if in $Map.Ifs) { Add-PsdMapWrites $if.Body $Prefix $Out } } # The first key path of Reads that Writes touches: the same key, or one inside the other. $null when there is none. function Find-PsdKeyOverlap { param($Reads, $Writes) foreach ($r in $Reads.PSBase.Keys) { foreach ($w in $Writes.PSBase.Keys) { if ($r -eq $w -or $r.StartsWith("$w.", [StringComparison]::Ordinal) -or $w.StartsWith("$r.", [StringComparison]::Ordinal)) { return $r } } } $null } function Get-PsdItemResult { param($Item, $Partial, $Doc) if ($Item.Kind -eq 'If') { return [bool](Test-PsdCondition $Item.Node.Cond @{ Doc = $Partial; Node = $Item.Node; InCond = $true; Final = $Doc }) } $value = Get-PsdNodeValue $Item.Node $Partial if ($value -is [string] -and $value.Length -gt 0) { return $value } if ($Doc.Ctx.Errors.Count -eq 0) { Add-PsdNodeError $Doc.Ctx 'PSD206' "this path is $(Get-PsdKindText $value), but it has to be text." $Item.Node } $null } function Add-PsdDependencyError { param($Open, $Ctx) $extra = New-PsdList foreach ($i in $Open) { $n = $i.Node $text = if ($i.Kind -eq 'If') { $n.Raw } else { $n.Text } $extra.Add("$($n.File.Name + $n.File.Ext), line $($n.Line): $text") } $extra.Add('') Add-PsdNodeError $Ctx 'PSD404' 'these conditions read what the others write, so there is no order to decide them in.' $Open[0].Node ` -Extra $extra -Hint @('Give one of the keys a value outside of the !if, or join the conditions.') } # A file loaded after a decision can change what it read: then the decision was wrong, and that is an error. function Test-PsdLateFiles { param($Doc, $Decided, $Files) if ($Decided.Count -eq 0) { return } $final = $null foreach ($item in $Decided) { foreach ($f in $Files.Values) { if ($f.Round -lt $item.Round -or @($f.Via) -contains $item.Id) { continue } $key = Find-PsdKeyOverlap $item.Reads $f.Writes if ($null -eq $key -or $null -ne (Find-PsdKeyOverlap $item.Reads $item.Writes)) { continue } if ($null -eq $final) { $final = New-PsdPartialDoc $Doc } $now = Get-PsdItemResult $item $final $Doc if ($now -ne $item.Result) { Add-PsdNodeError $Doc.Ctx 'PSD405' "this was decided as '$($item.Result)', but $($f.Name) was loaded after that and sets $key, which makes it '$now'." $item.Node ` -Hint @('Set the key in a file that is always loaded, or make the conditions not depend on it.') } break } } } # What Get-PsdLoadPlan returns for a document. function Get-PsdPlanResult { param($Doc) $steps = foreach ($item in $Doc.Steps) { $reads = [ordered]@{} foreach ($key in @($item.Reads.PSBase.Keys | Sort-Object)) { $cur = Get-PsdTreeNode $item.Partial.Tree $item.Reads[$key] $reads[$key] = if ($null -eq $cur) { $null } else { Get-PsdNodeValue $cur $item.Partial } } $node = $item.Node [pscustomobject]@{ Round = $item.Round Kind = $item.Kind File = $node.File.Path Line = $node.Line Text = if ($item.Kind -eq 'If') { $node.Raw } else { $node.Text } Result = $item.Result Reads = $reads } } [pscustomobject]@{ Files = @($Doc.Layers | ForEach-Object { $_.File.Path }) Steps = @($steps) } } # Decides everything open, loading files as they turn up, and leaves the final layers on the document. function Resolve-PsdDocFiles { param($Doc, $Sources) Update-PsdDocFiles $Doc $Sources $decided = New-PsdList $files = @{} $round = 0 while ($true) { foreach ($l in $Doc.Layers) { if ($files.ContainsKey($l.File.Key) -or $null -eq $l.File.Root) { continue } $writes = @{} Add-PsdMapWrites $l.File.Root '' $writes $files[$l.File.Key] = @{ Name = $l.File.Name + $l.File.Ext; Round = $round; Via = $l.Via; Writes = $writes } } $open = Get-PsdOpenItems $Doc if ($open.Count -eq 0) { break } $round++ $partial = New-PsdPartialDoc $Doc foreach ($item in $open) { $seen = @{} if ($item.Kind -eq 'If') { Add-PsdCondReads $item.Node.Cond $partial.Tree $item.Reads $seen Add-PsdMapWrites $item.Node.Body '' $item.Writes $item.Loads = Test-PsdMapLoads $item.Node.Body } else { Add-PsdNodeReads $item.Node $partial.Tree $item.Reads $seen } } $ready = New-PsdList foreach ($item in $open) { $blocked = $false foreach ($other in $open) { if ($other.Id -ne $item.Id -and $null -ne (Find-PsdKeyOverlap $item.Reads $other.Writes)) { $blocked = $true; break } } if (-not $blocked) { $ready.Add($item) } } if ($ready.Count -eq 0) { Add-PsdDependencyError $open $Doc.Ctx break } # what loads files goes first: those files can set what the other conditions read $loading = New-PsdList foreach ($item in $ready) { if ($item.Loads) { $loading.Add($item) } } if ($loading.Count) { $ready = $loading } foreach ($item in $ready) { $item.Round = $round $item.Partial = $partial $item.Result = Get-PsdItemResult $item $partial $Doc if ($item.Kind -eq 'If') { $Doc.Decisions[$item.Id] = $item.Result } else { $Doc.Paths[$item.Id] = $item.Result } $decided.Add($item) } Update-PsdDocFiles $Doc $Sources } $Doc.Settled = $true Update-PsdDocFiles $Doc $Sources Test-PsdLateFiles $Doc $decided $files $Doc.Steps = $decided } |