605 lines
26 KiB
PowerShell
605 lines
26 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Build script for the more_math ANTLR grammar and its generated symbol tables.
|
|
|
|
.DESCRIPTION
|
|
1. Compiles more_math/Parser/MathExpr.g4 into more_math/Parser/grammer
|
|
(the parser antlr_router.py loads for current antlr4-python3-runtime
|
|
versions), using whatever ANTLR toolchain is available on this
|
|
machine (an 'antlr4' launcher on PATH, or 'java -jar <jar>').
|
|
2. Parses MathExpr.g4 itself (keywords, constants, function tokens and
|
|
their doc-comment signatures/descriptions) and regenerates:
|
|
- more_math/Parser/inbuilt_symbols.py
|
|
- web/inbuilt_symbols.js
|
|
so that the Python interpreter and the web autocomplete UI both read
|
|
an up-to-date, identical view of the language's built-in symbols.
|
|
|
|
.PARAMETER SkipAntlr
|
|
Skip step 1 (grammar compilation) and only regenerate the symbol tables.
|
|
|
|
.PARAMETER AntlrJar
|
|
Path to an antlr-*-complete.jar to use for step 1. Defaults to
|
|
$env:ANTLR_JAR, then tools\antlr.jar next to this script.
|
|
|
|
.PARAMETER AntlrLegacyJar
|
|
Optional path to an ANTLR 4.9.x complete jar to use for regenerating
|
|
more_math/Parser/legacy (the antlr4-python3-runtime==4.9.x compatibility
|
|
path used by antlr_router.py). Defaults to $env:ANTLR_LEGACY_JAR. When
|
|
not given, the 'antlr4' launcher (antlr4-tools) is tried instead, pinned
|
|
to -AntlrLegacyVersion, same as the main grammar target.
|
|
|
|
.PARAMETER AntlrVersion
|
|
ANTLR release to use when the 'antlr4' launcher comes from the
|
|
antlr4-tools pip package (it otherwise queries Maven Central for the
|
|
latest release on every run). Defaults to $env:ANTLR4_TOOLS_ANTLR_VERSION,
|
|
then '4.13.2' to match the version more_math/Parser/grammer was last
|
|
generated with. Ignored when using a plain jar/java invocation.
|
|
|
|
.PARAMETER AntlrLegacyVersion
|
|
Same as -AntlrVersion, but for the more_math/Parser/legacy (ANTLR 4.9.x)
|
|
target. Defaults to $env:ANTLR4_TOOLS_ANTLR_LEGACY_VERSION, then '4.9.3'
|
|
to match the version more_math/Parser/legacy was last generated with.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$SkipAntlr,
|
|
[string]$AntlrJar = $env:ANTLR_JAR,
|
|
[string]$AntlrLegacyJar = $env:ANTLR_LEGACY_JAR,
|
|
[string]$AntlrVersion = $(if ($env:ANTLR4_TOOLS_ANTLR_VERSION) { $env:ANTLR4_TOOLS_ANTLR_VERSION } else { '4.13.2' }),
|
|
[string]$AntlrLegacyVersion = $(if ($env:ANTLR4_TOOLS_ANTLR_LEGACY_VERSION) { $env:ANTLR4_TOOLS_ANTLR_LEGACY_VERSION } else { '4.9.3' })
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# This script lives at more_math/Parser/compile.ps1, two levels under the
|
|
# node root (more_math/Parser -> more_math -> node root).
|
|
$ParserDir = $PSScriptRoot
|
|
$NodeRoot = Split-Path (Split-Path $ParserDir -Parent) -Parent
|
|
$GrammarFile = Join-Path $ParserDir 'MathExpr.g4'
|
|
$GrammarOutDir = Join-Path $ParserDir 'grammer'
|
|
$LegacyOutDir = Join-Path $ParserDir 'legacy'
|
|
$WebDir = Join-Path $NodeRoot 'web'
|
|
$PySymbolsPath = Join-Path $ParserDir 'inbuilt_symbols.py'
|
|
$JsSymbolsPath = Join-Path $WebDir 'inbuilt_symbols.js'
|
|
|
|
if (-not (Test-Path $GrammarFile)) {
|
|
throw "Grammar file not found: $GrammarFile"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 1: ANTLR compilation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Invoke-AntlrCompile {
|
|
# Runs with cwd = $ParserDir and passes ANTLR just the grammar's leaf
|
|
# filename, so the "# Generated from MathExpr.g4 by ANTLR ..." header it
|
|
# writes into the output files stays a plain relative name instead of
|
|
# embedding this machine's local absolute path.
|
|
param(
|
|
[Parameter(Mandatory)][string]$Grammar,
|
|
[Parameter(Mandatory)][string]$OutDir,
|
|
[string]$Jar,
|
|
[string]$Version = $AntlrVersion
|
|
)
|
|
|
|
$grammarLeaf = Split-Path -Leaf $Grammar
|
|
|
|
$antlrCmd = Get-Command antlr4 -ErrorAction SilentlyContinue
|
|
if ($antlrCmd) {
|
|
# antlr4-tools (the pip-installed launcher) otherwise hits Maven
|
|
# Central on every invocation to resolve "latest"; pin it so the
|
|
# build is reproducible and doesn't need that network call.
|
|
$previousVersionEnv = $env:ANTLR4_TOOLS_ANTLR_VERSION
|
|
$env:ANTLR4_TOOLS_ANTLR_VERSION = $Version
|
|
Push-Location $ParserDir
|
|
try {
|
|
Write-Host "[antlr] antlr4 (v$Version) -Dlanguage=Python3 -visitor -o `"$OutDir`" `"$grammarLeaf`""
|
|
& $antlrCmd.Source -Dlanguage=Python3 -visitor -o $OutDir $grammarLeaf
|
|
if ($LASTEXITCODE -ne 0) { throw "antlr4 failed with exit code $LASTEXITCODE" }
|
|
} finally {
|
|
Pop-Location
|
|
$env:ANTLR4_TOOLS_ANTLR_VERSION = $previousVersionEnv
|
|
}
|
|
return $true
|
|
}
|
|
|
|
$javaCmd = Get-Command java -ErrorAction SilentlyContinue
|
|
if (-not $javaCmd) {
|
|
return $false
|
|
}
|
|
|
|
if (-not $Jar) {
|
|
$default = Join-Path $NodeRoot 'tools\antlr.jar'
|
|
if (Test-Path $default) { $Jar = $default }
|
|
}
|
|
|
|
if (-not $Jar -or -not (Test-Path $Jar)) {
|
|
return $false
|
|
}
|
|
|
|
Push-Location $ParserDir
|
|
try {
|
|
Write-Host "[antlr] java -jar `"$Jar`" -Dlanguage=Python3 -visitor -o `"$OutDir`" `"$grammarLeaf`""
|
|
& $javaCmd.Source -jar $Jar -Dlanguage=Python3 -visitor -o $OutDir $grammarLeaf
|
|
if ($LASTEXITCODE -ne 0) { throw "ANTLR jar invocation failed with exit code $LASTEXITCODE" }
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
return $true
|
|
}
|
|
|
|
if ($SkipAntlr) {
|
|
Write-Host "[antlr] Skipping grammar compilation (-SkipAntlr)."
|
|
} else {
|
|
$ok = Invoke-AntlrCompile -Grammar $GrammarFile -OutDir $GrammarOutDir -Jar $AntlrJar
|
|
if (-not $ok) {
|
|
Write-Warning ("ANTLR toolchain not found (no 'antlr4' on PATH and no usable jar). " +
|
|
"Skipping grammar compilation; '$GrammarOutDir' was left untouched. " +
|
|
"Install Java + antlr4-tools ('pip install antlr4-tools'), or set `$env:ANTLR_JAR " +
|
|
"to an antlr-*-complete.jar, then re-run.")
|
|
}
|
|
|
|
if ($AntlrLegacyJar -and -not (Test-Path $AntlrLegacyJar)) {
|
|
Write-Warning "AntlrLegacyJar '$AntlrLegacyJar' does not exist; falling back to the antlr4 launcher for the legacy target."
|
|
$AntlrLegacyJar = $null
|
|
}
|
|
|
|
$legacyOk = Invoke-AntlrCompile -Grammar $GrammarFile -OutDir $LegacyOutDir -Jar $AntlrLegacyJar -Version $AntlrLegacyVersion
|
|
if (-not $legacyOk) {
|
|
Write-Warning ("ANTLR toolchain not found for the legacy (ANTLR 4.9.x) target. " +
|
|
"Skipping legacy grammar compilation; '$LegacyOutDir' was left untouched. " +
|
|
"Install antlr4-tools, or set `$env:ANTLR_LEGACY_JAR to an antlr-4.9.x-complete.jar, then re-run.")
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 2: parse MathExpr.g4 for its built-in symbol table
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Get-GrammarSections {
|
|
param([Parameter(Mandatory)][string]$Text)
|
|
|
|
$funcStart = $Text.IndexOf('func0:')
|
|
$lexerMarker = '// LEXER RULES'
|
|
$lexerStart = $Text.IndexOf($lexerMarker)
|
|
if ($funcStart -lt 0 -or $lexerStart -lt 0 -or $lexerStart -le $funcStart) {
|
|
throw "Could not locate expected grammar sections ('func0:' / '$lexerMarker') - has MathExpr.g4's layout changed?"
|
|
}
|
|
|
|
[pscustomobject]@{
|
|
FuncSection = $Text.Substring($funcStart, $lexerStart - $funcStart)
|
|
LexerSection = $Text.Substring($lexerStart)
|
|
}
|
|
}
|
|
|
|
function Get-LexerTokens {
|
|
# Returns an ordered map of TOKEN_NAME -> raw string-literal alternatives,
|
|
# for every lexer rule that defines at least one word-like literal
|
|
# (i.e. skips punctuation/operator tokens and regex-only tokens like
|
|
# NUMBER/STRING/VARIABLE/WS/comments, which have no literal alternatives).
|
|
param([Parameter(Mandatory)][string]$LexerSection)
|
|
|
|
$tokenRegex = [regex]'(?ms)^[ \t]*([A-Z][A-Z0-9_]*)[ \t]*:[ \t]*(.*?);'
|
|
$literalRegex = [regex]"'([a-zA-Z_][a-zA-Z0-9_]*)'"
|
|
|
|
$tokens = [ordered]@{}
|
|
foreach ($m in $tokenRegex.Matches($LexerSection)) {
|
|
$name = $m.Groups[1].Value
|
|
$body = $m.Groups[2].Value
|
|
$literals = @($literalRegex.Matches($body) | ForEach-Object { $_.Groups[1].Value })
|
|
if ($literals.Count -gt 0) {
|
|
$tokens[$name] = $literals
|
|
}
|
|
}
|
|
return $tokens
|
|
}
|
|
|
|
function Get-FunctionTokenNames {
|
|
# A token counts as a "function" token when it is used as `TOKEN LPAREN`
|
|
# inside the func0..funcNoise rules (the atom productions for calls).
|
|
# This deliberately excludes structural keywords like IF/WHILE/FOR, which
|
|
# are also followed by LPAREN but in ifStmt/whileStmt/forStmt, outside
|
|
# this section.
|
|
param([Parameter(Mandatory)][string]$FuncSection)
|
|
|
|
$names = New-Object System.Collections.Generic.HashSet[string]
|
|
foreach ($m in [regex]::Matches($FuncSection, '\b([A-Z][A-Z0-9_]*)\s+LPAREN\b')) {
|
|
[void]$names.Add($m.Groups[1].Value)
|
|
}
|
|
return $names
|
|
}
|
|
|
|
function Get-UniqueWords {
|
|
# De-duplicates literal alternatives, preserving first-seen order. Case
|
|
# variants (e.g. 'pi'/'PI', 'none'/'None'/'null'/'NULL') are all kept,
|
|
# since the lexer is case-sensitive and the web highlighter must
|
|
# recognise every spelling the grammar accepts.
|
|
param([string[]]$Words)
|
|
|
|
$seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal)
|
|
$result = New-Object System.Collections.Generic.List[string]
|
|
foreach ($w in $Words) {
|
|
if ($seen.Add($w)) { [void]$result.Add($w) }
|
|
}
|
|
return $result
|
|
}
|
|
|
|
function Get-FunctionDocEntries {
|
|
# Pairs each `/** ... */` doc comment in the func section with the
|
|
# lexer token of the call it documents (the `TOKEN LPAREN` that follows
|
|
# the comment, skipping over an optional leading `|` alternation).
|
|
param([Parameter(Mandatory)][string]$FuncSection)
|
|
|
|
$pattern = [regex]'(?s)/\*\*(?<doc>.*?)\*/\s*\|?\s*(?<token>[A-Z][A-Z0-9_]*)\s+LPAREN'
|
|
$entries = New-Object System.Collections.Generic.List[object]
|
|
foreach ($m in $pattern.Matches($FuncSection)) {
|
|
$lines = @($m.Groups['doc'].Value -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
|
|
if ($lines.Count -eq 0) { continue }
|
|
$entries.Add([pscustomobject]@{
|
|
Token = $m.Groups['token'].Value
|
|
FirstLine = $lines[0]
|
|
Details = @($lines | Select-Object -Skip 1)
|
|
})
|
|
}
|
|
return $entries
|
|
}
|
|
|
|
function Get-ProductionArity {
|
|
# Computes argument-count bounds from a call production's argument list,
|
|
# e.g. "expr COMMA expr (COMMA expr)?" -> 2..3. A `*`/`+` group makes the
|
|
# call variadic. This is what the parser actually accepts.
|
|
param([string]$ArgsText)
|
|
|
|
$stack = New-Object System.Collections.Generic.List[hashtable]
|
|
$stack.Add(@{ Min = 0; Max = 0; Variadic = $false })
|
|
foreach ($m in [regex]::Matches($ArgsText, '\(|\)[?*+]?|\b(?:expr|indexExpr)\b')) {
|
|
$t = $m.Value
|
|
if ($t -eq '(') {
|
|
$stack.Add(@{ Min = 0; Max = 0; Variadic = $false })
|
|
continue
|
|
}
|
|
$top = $stack[$stack.Count - 1]
|
|
if ($t.StartsWith(')')) {
|
|
$stack.RemoveAt($stack.Count - 1)
|
|
$parent = $stack[$stack.Count - 1]
|
|
$q = $t.Substring(1)
|
|
if ($q -eq '' -or $q -eq '+') { $parent.Min += $top.Min }
|
|
$parent.Max += $top.Max
|
|
$parent.Variadic = $parent.Variadic -or $top.Variadic -or $q -eq '*' -or $q -eq '+'
|
|
continue
|
|
}
|
|
$top.Min += 1
|
|
$top.Max += 1
|
|
}
|
|
|
|
$root = $stack[0]
|
|
[pscustomobject]@{ MinArgs = $root.Min; MaxArgs = $(if ($root.Variadic) { $null } else { $root.Max }) }
|
|
}
|
|
|
|
function Get-ProductionArities {
|
|
# Maps each function token to the arity of its `TOKEN LPAREN ... RPAREN` production.
|
|
param([Parameter(Mandatory)][string]$FuncSection)
|
|
|
|
$code = [regex]::Replace($FuncSection, '(?s)/\*.*?\*/', '')
|
|
$arities = @{}
|
|
foreach ($m in [regex]::Matches($code, '\b([A-Z][A-Z0-9_]*)\s+LPAREN(.*?)\bRPAREN\b')) {
|
|
$arities[$m.Groups[1].Value] = Get-ProductionArity -ArgsText $m.Groups[2].Value
|
|
}
|
|
return $arities
|
|
}
|
|
|
|
function Split-ArgSpec {
|
|
# Parses a documented arg list like "x, [dims]", "r, [g, b]" or
|
|
# "tensor, [kernel_sizes...], kernel" into min/max argument counts and
|
|
# display names (kept as written, brackets included). Args inside [...]
|
|
# are optional; an arg containing "..." makes the call variadic.
|
|
param([string]$ArgsText)
|
|
|
|
$min = 0
|
|
$max = 0
|
|
$variadic = $false
|
|
$depth = 0
|
|
$display = New-Object System.Collections.Generic.List[string]
|
|
|
|
foreach ($raw in ($ArgsText -split ',')) {
|
|
$p = $raw.Trim()
|
|
if ($p -eq '') { continue }
|
|
$isOptional = $depth -gt 0 -or $p.StartsWith('[')
|
|
$depth += ([regex]::Matches($p, '\[')).Count - ([regex]::Matches($p, '\]')).Count
|
|
[void]$display.Add($p)
|
|
if ($p.Contains('...')) { $variadic = $true; continue }
|
|
$max++
|
|
if (-not $isOptional) { $min++ }
|
|
}
|
|
|
|
[pscustomobject]@{ Min = $min; Max = $max; Variadic = $variadic; Display = $display.ToArray() }
|
|
}
|
|
|
|
function ConvertTo-FunctionSignature {
|
|
# Parses a doc-comment first line such as:
|
|
# "sum(x, [dims]) - computes the sum of elements of x, ..."
|
|
# "rgb_to_hsv(rgb, [degrees]) / rgb_to_hsv(r, g, b, [degrees]) - converts ..."
|
|
# into the signature text, the display arg names of every call form, the
|
|
# arg-count bounds covering all forms, and the trailing description text.
|
|
param([Parameter(Mandatory)][string]$Line)
|
|
|
|
$formPattern = [regex]'^\s*/?\s*[a-zA-Z_][a-zA-Z0-9_]*\((?<args>.*?)\)'
|
|
$rest = $Line
|
|
$forms = New-Object System.Collections.Generic.List[object]
|
|
$min = $null
|
|
$max = 0
|
|
$variadic = $false
|
|
while (($m = $formPattern.Match($rest)).Success -and ($forms.Count -eq 0 -or $m.Value.TrimStart().StartsWith('/'))) {
|
|
$spec = Split-ArgSpec -ArgsText $m.Groups['args'].Value
|
|
$forms.Add($spec.Display)
|
|
if ($null -eq $min -or $spec.Min -lt $min) { $min = $spec.Min }
|
|
if ($spec.Max -gt $max) { $max = $spec.Max }
|
|
if ($spec.Variadic) { $variadic = $true }
|
|
$rest = $rest.Substring($m.Length)
|
|
}
|
|
if ($forms.Count -eq 0) { return $null }
|
|
$signature = $Line.Substring(0, $Line.Length - $rest.Length).Trim()
|
|
|
|
$descMatch = [regex]::Match($rest, '^\s*-\s*(?<desc>.*)$')
|
|
$desc = if ($descMatch.Success) { $descMatch.Groups['desc'].Value.Trim() } else { $rest.Trim() }
|
|
|
|
[pscustomobject]@{
|
|
Signature = $signature
|
|
MinArgs = $min
|
|
MaxArgs = if ($variadic) { $null } else { $max }
|
|
Forms = $forms.ToArray()
|
|
Description = $desc
|
|
}
|
|
}
|
|
|
|
Write-Host "[symbols] Parsing $GrammarFile"
|
|
$grammarText = Get-Content -Raw -LiteralPath $GrammarFile
|
|
$sections = Get-GrammarSections -Text $grammarText
|
|
$allTokens = Get-LexerTokens -LexerSection $sections.LexerSection
|
|
$functionTokenNames = Get-FunctionTokenNames -FuncSection $sections.FuncSection
|
|
|
|
$constantWords = @()
|
|
$keywordWords = New-Object System.Collections.Generic.List[string]
|
|
$functionWords = New-Object System.Collections.Generic.List[string]
|
|
$functionTokenToWords = [ordered]@{}
|
|
|
|
$tokenNames = @($allTokens.psbase.Keys)
|
|
foreach ($name in $tokenNames) {
|
|
$words = Get-UniqueWords -Words $allTokens[$name]
|
|
if ($name -eq 'CONSTANT') {
|
|
$constantWords = $words
|
|
continue
|
|
}
|
|
if ($functionTokenNames.Contains($name)) {
|
|
$functionTokenToWords[$name] = $words
|
|
foreach ($w in $words) { [void]$functionWords.Add($w) }
|
|
} else {
|
|
foreach ($w in $words) { [void]$keywordWords.Add($w) }
|
|
}
|
|
}
|
|
|
|
$KEYWORDS = @($keywordWords | Sort-Object -Unique -CaseSensitive)
|
|
$CONSTANTS = @($constantWords | Sort-Object -Unique -CaseSensitive)
|
|
$FUNCTIONS = @($functionWords | Sort-Object -Unique -CaseSensitive)
|
|
|
|
$docEntries = Get-FunctionDocEntries -FuncSection $sections.FuncSection
|
|
$sigByToken = @{}
|
|
$detailsByToken = @{}
|
|
$paramsByToken = @{}
|
|
$paramTotal = 0
|
|
$paramDescribed = 0
|
|
foreach ($e in $docEntries) {
|
|
$sig = ConvertTo-FunctionSignature -Line $e.FirstLine
|
|
if (-not $sig) { continue }
|
|
$sigByToken[$e.Token] = $sig
|
|
|
|
# "- name: description" lines document one parameter; other lines are general details.
|
|
$argNames = @($sig.Forms | ForEach-Object { $_ } | ForEach-Object { $_.Trim('[', ']', ' ').Replace('...', '') } | Where-Object { $_ })
|
|
$params = [ordered]@{}
|
|
$details = New-Object System.Collections.Generic.List[string]
|
|
foreach ($line in $e.Details) {
|
|
$pm = [regex]::Match($line, '^-\s*(?<name>[a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(?<desc>.+)$')
|
|
if (-not $pm.Success) {
|
|
[void]$details.Add($line)
|
|
continue
|
|
}
|
|
$paramName = $pm.Groups['name'].Value
|
|
if ($argNames -notcontains $paramName) {
|
|
Write-Warning "Doc comment for $($e.Token) describes parameter '$paramName', which is not in '$($sig.Signature)'."
|
|
}
|
|
$params[$paramName] = $pm.Groups['desc'].Value.Trim()
|
|
}
|
|
$paramsByToken[$e.Token] = $params
|
|
$detailsByToken[$e.Token] = $details -join "`n"
|
|
$paramTotal += @($argNames | Select-Object -Unique).Count
|
|
$paramDescribed += @($argNames | Select-Object -Unique | Where-Object { $params.Contains($_) }).Count
|
|
}
|
|
Write-Host "[symbols] $paramDescribed of $paramTotal parameters have a description."
|
|
|
|
$functionTokens = @($functionTokenToWords.psbase.Keys)
|
|
$undocumented = @($functionTokens | Where-Object { -not $sigByToken.ContainsKey($_) })
|
|
if ($undocumented.Count -gt 0) {
|
|
Write-Warning "No parsable doc comment found for function token(s): $($undocumented -join ', '). They will get placeholder metadata."
|
|
}
|
|
|
|
# The grammar production bounds what parses; the doc comment may be stricter
|
|
# (e.g. conv accepts any count in the grammar but 3..5 in the visitor). Use the
|
|
# narrower range, and report docs that allow calls the grammar rejects.
|
|
$arityByToken = Get-ProductionArities -FuncSection $sections.FuncSection
|
|
function Format-Arity { param($Min, $Max) "$Min..$(if ($null -eq $Max) { 'n' } else { $Max })" }
|
|
function Test-ArityWithin {
|
|
param($Inner, $Outer)
|
|
$Inner.MinArgs -ge $Outer.MinArgs -and ($null -eq $Outer.MaxArgs -or ($null -ne $Inner.MaxArgs -and $Inner.MaxArgs -le $Outer.MaxArgs))
|
|
}
|
|
|
|
$FUNCTION_META = [ordered]@{}
|
|
foreach ($token in $functionTokens) {
|
|
$sig = $sigByToken[$token]
|
|
$arity = $arityByToken[$token]
|
|
if ($sig -and $arity) {
|
|
if (Test-ArityWithin $sig $arity) {
|
|
$arity = $sig
|
|
} else {
|
|
Write-Warning "Doc comment for $token ('$($sig.Signature)') documents $(Format-Arity $sig.MinArgs $sig.MaxArgs) args, grammar accepts $(Format-Arity $arity.MinArgs $arity.MaxArgs)."
|
|
}
|
|
}
|
|
|
|
# Assigned outside an if-expression so PowerShell doesn't unroll the nested arrays.
|
|
$forms = [object[]]@(, [string[]]@())
|
|
if ($sig) { $forms = $sig.Forms }
|
|
|
|
$words = $functionTokenToWords[$token]
|
|
foreach ($alias in $words) {
|
|
# Show the signature under the spelling being completed (e.g. conv vs convolution).
|
|
$signature = if ($sig) { [regex]::Replace($sig.Signature, '(^|/\s*)[a-zA-Z_][a-zA-Z0-9_]*(?=\()', "`${1}$alias") } else { "$alias()" }
|
|
$FUNCTION_META[$alias] = [pscustomobject]@{
|
|
MinArgs = if ($arity) { $arity.MinArgs } else { 0 }
|
|
MaxArgs = if ($arity) { $arity.MaxArgs } else { $null }
|
|
Signature = $signature
|
|
Forms = $forms
|
|
Description = if ($sig) { $sig.Description } else { '' }
|
|
Details = if ($sig) { $detailsByToken[$token] } else { '' }
|
|
Params = if ($sig) { $paramsByToken[$token] } else { [ordered]@{} }
|
|
Aliases = @($words | Where-Object { $_ -ne $alias })
|
|
}
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 3: emit the generated files
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Set-Utf8NoBom {
|
|
# Windows PowerShell 5.1's Set-Content has no utf8NoBOM encoding, so write
|
|
# the file directly via .NET to avoid a BOM in the generated .py/.js files.
|
|
param([string]$Path, [string]$Content)
|
|
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
[System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
|
|
}
|
|
|
|
function ConvertTo-JsString {
|
|
param([string]$Value)
|
|
$escaped = $Value.Replace('\', '\\').Replace('"', '\"').Replace("`n", '\n')
|
|
return '"' + $escaped + '"'
|
|
}
|
|
|
|
function ConvertTo-PyString {
|
|
param([string]$Value)
|
|
$escaped = $Value.Replace('\', '\\').Replace("'", "\'").Replace("`n", '\n')
|
|
return "'" + $escaped + "'"
|
|
}
|
|
|
|
function ConvertTo-JsArray {
|
|
param([string[]]$Values)
|
|
# An empty array arrives as $null after passing through an if-expression.
|
|
if (-not $Values) { return '[]' }
|
|
return '[' + (@($Values | ForEach-Object { ConvertTo-JsString $_ }) -join ', ') + ']'
|
|
}
|
|
|
|
function ConvertTo-PyList {
|
|
param([string[]]$Values)
|
|
# An empty array arrives as $null after passing through an if-expression.
|
|
if (-not $Values) { return '[]' }
|
|
return '[' + (@($Values | ForEach-Object { ConvertTo-PyString $_ }) -join ', ') + ']'
|
|
}
|
|
|
|
function ConvertTo-JsForms {
|
|
param([object[]]$Forms)
|
|
$parts = foreach ($form in $Forms) { ConvertTo-JsArray $form }
|
|
return '[' + (@($parts) -join ', ') + ']'
|
|
}
|
|
|
|
function ConvertTo-PyForms {
|
|
param([object[]]$Forms)
|
|
$parts = foreach ($form in $Forms) { ConvertTo-PyList $form }
|
|
return '[' + (@($parts) -join ', ') + ']'
|
|
}
|
|
|
|
function ConvertTo-JsObject {
|
|
param([System.Collections.IDictionary]$Map)
|
|
$parts = foreach ($key in @($Map.Keys)) { "$(ConvertTo-JsString $key): $(ConvertTo-JsString $Map[$key])" }
|
|
return '{' + (@($parts) -join ', ') + '}'
|
|
}
|
|
|
|
function ConvertTo-PyDict {
|
|
param([System.Collections.IDictionary]$Map)
|
|
$parts = foreach ($key in @($Map.Keys)) { "$(ConvertTo-PyString $key): $(ConvertTo-PyString $Map[$key])" }
|
|
return '{' + (@($parts) -join ', ') + '}'
|
|
}
|
|
|
|
function Write-JsSymbols {
|
|
param(
|
|
[string]$Path,
|
|
[string[]]$Keywords,
|
|
[string[]]$Constants,
|
|
[string[]]$Functions,
|
|
[System.Collections.Specialized.OrderedDictionary]$FunctionMeta
|
|
)
|
|
|
|
$sb = New-Object System.Text.StringBuilder
|
|
[void]$sb.AppendLine('// Generated automatically by compile.ps1. Do not edit.')
|
|
[void]$sb.AppendLine()
|
|
[void]$sb.AppendLine('export const KEYWORDS = new Set([')
|
|
[void]$sb.AppendLine(($Keywords | ForEach-Object { ConvertTo-JsString $_ }) -join ', ')
|
|
[void]$sb.AppendLine(']);')
|
|
[void]$sb.AppendLine()
|
|
[void]$sb.AppendLine('export const CONSTANTS = new Set([')
|
|
[void]$sb.AppendLine(($Constants | ForEach-Object { ConvertTo-JsString $_ }) -join ', ')
|
|
[void]$sb.AppendLine(']);')
|
|
[void]$sb.AppendLine()
|
|
[void]$sb.AppendLine('export const FUNCTIONS = new Set([')
|
|
[void]$sb.AppendLine(($Functions | ForEach-Object { ConvertTo-JsString $_ }) -join ', ')
|
|
[void]$sb.AppendLine(']);')
|
|
[void]$sb.AppendLine()
|
|
[void]$sb.AppendLine('export const FUNCTION_META = {')
|
|
foreach ($name in @($FunctionMeta.psbase.Keys)) {
|
|
$meta = $FunctionMeta[$name]
|
|
$maxArgsJs = if ($null -eq $meta.MaxArgs) { 'null' } else { $meta.MaxArgs }
|
|
[void]$sb.AppendLine(" $(ConvertTo-JsString $name): { minArgs: $($meta.MinArgs), maxArgs: $maxArgsJs, signature: $(ConvertTo-JsString $meta.Signature), forms: $(ConvertTo-JsForms $meta.Forms), description: $(ConvertTo-JsString $meta.Description), details: $(ConvertTo-JsString $meta.Details), params: $(ConvertTo-JsObject $meta.Params), aliases: $(ConvertTo-JsArray $meta.Aliases) },")
|
|
}
|
|
[void]$sb.AppendLine('};')
|
|
|
|
Set-Utf8NoBom -Path $Path -Content $sb.ToString()
|
|
}
|
|
|
|
function Write-PySymbols {
|
|
param(
|
|
[string]$Path,
|
|
[string[]]$Keywords,
|
|
[string[]]$Constants,
|
|
[string[]]$Functions,
|
|
[System.Collections.Specialized.OrderedDictionary]$FunctionMeta
|
|
)
|
|
|
|
$sb = New-Object System.Text.StringBuilder
|
|
[void]$sb.AppendLine('# Generated automatically by compile.ps1. Do not edit.')
|
|
[void]$sb.AppendLine('INBUILT_KEYWORDS = {')
|
|
[void]$sb.AppendLine(($Keywords | ForEach-Object { ConvertTo-PyString $_ }) -join ', ')
|
|
[void]$sb.AppendLine('}')
|
|
[void]$sb.AppendLine('INBUILT_CONSTANTS = {')
|
|
[void]$sb.AppendLine(($Constants | ForEach-Object { ConvertTo-PyString $_ }) -join ', ')
|
|
[void]$sb.AppendLine('}')
|
|
[void]$sb.AppendLine('INBUILT_FUNCTIONS = {')
|
|
[void]$sb.AppendLine(($Functions | ForEach-Object { ConvertTo-PyString $_ }) -join ', ')
|
|
[void]$sb.AppendLine('}')
|
|
[void]$sb.AppendLine()
|
|
[void]$sb.AppendLine('INBUILT_FUNCTION_META = {')
|
|
foreach ($name in @($FunctionMeta.psbase.Keys)) {
|
|
$meta = $FunctionMeta[$name]
|
|
$maxArgsPy = if ($null -eq $meta.MaxArgs) { 'None' } else { $meta.MaxArgs }
|
|
[void]$sb.AppendLine(" $(ConvertTo-PyString $name): {'min_args': $($meta.MinArgs), 'max_args': $maxArgsPy, 'signature': $(ConvertTo-PyString $meta.Signature), 'forms': $(ConvertTo-PyForms $meta.Forms), 'description': $(ConvertTo-PyString $meta.Description), 'details': $(ConvertTo-PyString $meta.Details), 'params': $(ConvertTo-PyDict $meta.Params), 'aliases': $(ConvertTo-PyList $meta.Aliases)},")
|
|
}
|
|
[void]$sb.AppendLine('}')
|
|
|
|
Set-Utf8NoBom -Path $Path -Content $sb.ToString()
|
|
}
|
|
|
|
Write-Host "[symbols] Writing $PySymbolsPath"
|
|
Write-PySymbols -Path $PySymbolsPath -Keywords $KEYWORDS -Constants $CONSTANTS -Functions $FUNCTIONS -FunctionMeta $FUNCTION_META
|
|
|
|
Write-Host "[symbols] Writing $JsSymbolsPath"
|
|
Write-JsSymbols -Path $JsSymbolsPath -Keywords $KEYWORDS -Constants $CONSTANTS -Functions $FUNCTIONS -FunctionMeta $FUNCTION_META
|
|
|
|
Write-Host "[symbols] $($FUNCTIONS.Count) functions, $($KEYWORDS.Count) keywords, $($CONSTANTS.Count) constants."
|
|
Write-Host 'Done.'
|