mlld
Version:
mlld: llm scripting language
1,472 lines (1,468 loc) • 4.87 MB
JavaScript
#!/usr/bin/env node
'use strict';
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
__defProp(target, "default", { value: mod, enumerable: true }) ,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// services/fs/NodeFileSystem.ts
var fs, path, _NodeFileSystem, NodeFileSystem;
var init_NodeFileSystem = __esm({
"services/fs/NodeFileSystem.ts"() {
fs = __toESM(require("fs/promises"));
path = __toESM(require("path"));
_NodeFileSystem = class _NodeFileSystem {
async readFile(filePath) {
return await fs.readFile(filePath, "utf-8");
}
async writeFile(filePath, content) {
const dir = path.dirname(filePath);
await fs.mkdir(dir, {
recursive: true
});
await fs.writeFile(filePath, content, "utf-8");
}
async appendFile(filePath, content) {
const dir = path.dirname(filePath);
await fs.mkdir(dir, {
recursive: true
});
await fs.appendFile(filePath, content, "utf-8");
}
async exists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
async mkdir(dirPath, options) {
await fs.mkdir(dirPath, options);
}
async readdir(dirPath) {
return await fs.readdir(dirPath);
}
async isDirectory(filePath) {
try {
const stats = await fs.stat(filePath);
return stats.isDirectory();
} catch {
return false;
}
}
async stat(filePath) {
const stats = await fs.stat(filePath);
return {
isDirectory: /* @__PURE__ */ __name(() => stats.isDirectory(), "isDirectory"),
isFile: /* @__PURE__ */ __name(() => stats.isFile(), "isFile")
};
}
};
__name(_NodeFileSystem, "NodeFileSystem");
NodeFileSystem = _NodeFileSystem;
}
});
// services/fs/PathService.ts
var path2, _PathService, PathService;
var init_PathService = __esm({
"services/fs/PathService.ts"() {
path2 = __toESM(require("path"));
_PathService = class _PathService {
// Basic path operations
resolve(...segments) {
return path2.resolve(...segments);
}
relative(from, to) {
return path2.relative(from, to);
}
join(...segments) {
return path2.join(...segments);
}
dirname(filePath) {
return path2.dirname(filePath);
}
basename(filePath, ext) {
return path2.basename(filePath, ext);
}
extname(filePath) {
return path2.extname(filePath);
}
isAbsolute(filePath) {
return path2.isAbsolute(filePath);
}
normalize(filePath) {
return path2.normalize(filePath);
}
// URL support
isURL(path74) {
try {
const url = new URL(path74);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}
async validateURL(url) {
if (!this.isURL(url)) {
throw new Error(`Invalid URL: ${url}`);
}
return url;
}
async fetchURL(url) {
await this.validateURL(url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`);
}
const content = await response.text();
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
return {
content,
headers
};
}
};
__name(_PathService, "PathService");
PathService = _PathService;
}
});
// grammar/generated/parser/grammar-core.js
var import_crypto, acorn, NodeType, DirectiveKind, warningCollector, helpers;
var init_grammar_core = __esm({
"grammar/generated/parser/grammar-core.js"() {
import_crypto = require("crypto");
acorn = __toESM(require("acorn"));
NodeType = {
Text: "Text",
Comment: "Comment",
CodeFence: "CodeFence",
MlldRunBlock: "MlldRunBlock",
VariableReference: "VariableReference",
Directive: "Directive",
PathSeparator: "PathSeparator",
DotSeparator: "DotSeparator",
Literal: "Literal",
SectionMarker: "SectionMarker",
Error: "Error",
Newline: "Newline",
StringLiteral: "StringLiteral",
Frontmatter: "Frontmatter",
CommandBase: "CommandBase",
Parameter: "Parameter",
ExecInvocation: "ExecInvocation",
CommandReference: "CommandReference",
FileReference: "FileReference",
BinaryExpression: "BinaryExpression",
TernaryExpression: "TernaryExpression",
UnaryExpression: "UnaryExpression",
WhenExpression: "WhenExpression"
};
DirectiveKind = {
run: "run",
var: "var",
show: "show",
stream: "stream",
exe: "exe",
for: "for",
path: "path",
import: "import",
export: "export",
output: "output",
append: "append",
when: "when",
guard: "guard",
// NO deprecated entries - clean break!
needs: "needs",
wants: "wants",
policy: "policy",
while: "while"
};
warningCollector = null;
helpers = {
debug(msg, ...args) {
if (process.env.DEBUG_MLLD_GRAMMAR) console.log("[DEBUG GRAMMAR]", msg, ...args);
},
warn(message, suggestion, loc, code) {
const warning = {
message,
...suggestion ? {
suggestion
} : {},
...loc ? {
location: loc
} : {},
...code ? {
code
} : {}
};
if (warningCollector) {
try {
warningCollector(warning);
return warning;
} catch {
}
}
try {
console.warn(`[mlld grammar warning] ${warning.message}`);
} catch {
}
return warning;
},
setWarningCollector(collector) {
if (!collector) {
warningCollector = null;
return;
}
if (Array.isArray(collector)) {
warningCollector = /* @__PURE__ */ __name((warning) => {
collector.push(warning);
}, "warningCollector");
return;
}
warningCollector = collector;
},
clearWarningCollector() {
warningCollector = null;
},
isExecutableReference(ref) {
if (!ref) return false;
if (ref.type === "ExecInvocation") return true;
if (ref.type === "FieldAccessExec") return true;
if (ref.arguments !== void 0 && ref.arguments !== null) return true;
if (ref.hasParentheses === true) return true;
return false;
},
isLogicalLineStart(input, pos) {
if (pos === 0) return true;
let i = pos - 1;
while (i >= 0 && " \r".includes(input[i])) i--;
return i < 0 || input[i] === "\n";
},
// Context Detection System - Core Helper Methods
// ---------------------------------------------
/**
* Determines if the current position represents a directive context.
* A directive context requires:
* 1. Logical line start
* 2. Optional leading slash
* 3. Followed by a directive keyword
*/
isDirectiveContext(input, pos) {
if (!this.isLogicalLineStart(input, pos)) return false;
let cursor = pos;
if (input[cursor] === "/") cursor++;
const directiveKeywords = [
...Object.keys(DirectiveKind),
"log"
];
for (const keyword of directiveKeywords) {
const end = cursor + keyword.length;
if (end > input.length) continue;
const potentialKeyword = input.substring(cursor, end);
if (potentialKeyword !== keyword) continue;
if (end === input.length) return true;
const nextChar = input[end];
if (" \r\n".includes(nextChar)) return true;
}
return false;
},
/**
* Legacy helper retained for compatibility.
* Delegates to isDirectiveContext but requires the slash prefix.
*/
isSlashDirectiveContext(input, pos) {
return input[pos] === "/" && this.isDirectiveContext(input, pos);
},
/**
* Determines if the current position represents a variable reference context
* A variable context requires:
* 1. @ symbol NOT at logical line start, or
* 2. @ at line start but NOT followed by directive keyword
*/
isAtVariableContext(input, pos) {
if (input[pos] !== "@") return false;
if (this.isDirectiveContext(input, pos)) return false;
return true;
},
/**
* DEPRECATED: RHS slashes are no longer supported
* Keeping for reference but this should not be used
* @deprecated
*/
isRHSContext(input, pos) {
return false;
},
/**
* Determines if the current position represents plain text context
* Plain text is any context that isn't a directive, variable, or RHS
*/
isPlainTextContext(input, pos) {
return !this.isDirectiveContext(input, pos) && !this.isAtVariableContext(input, pos);
},
/**
* Determines if the current position is within a run code block context
* This is used to identify language + code block patterns
*/
isInRunCodeBlockContext(input, pos) {
return false;
},
createNode(type, props) {
if (!props.location && process.env.DEBUG_MLLD_GRAMMAR) {
console.warn(`WARNING: Creating ${type} node without location data`);
if (process.env.DEBUG_MLLD_GRAMMAR_TRACE) {
console.trace();
}
}
return Object.freeze({
type,
nodeId: (0, import_crypto.randomUUID)(),
location: props.location,
...props
});
},
createDirective(kind, data) {
return this.createNode(NodeType.Directive, {
directive: {
kind,
...data
}
});
},
// New method for creating directives with the updated structure
createStructuredDirective(kind, subtype, values, raw, meta, locationData, source = null) {
return this.createNode(NodeType.Directive, {
kind,
subtype,
source,
values,
raw,
meta,
location: locationData
});
},
createVariableReferenceNode(valueType, data, location) {
if (!location) {
throw new Error(`Location is required for createVariableReferenceNode (valueType: ${valueType}, identifier: ${data.identifier || "unknown"})`);
}
return this.createNode(NodeType.VariableReference, {
valueType,
...data,
location
});
},
normalizePathVar(id) {
return id;
},
validateRunContent: /* @__PURE__ */ __name(() => true, "validateRunContent"),
validateDefineContent: /* @__PURE__ */ __name(() => true, "validateDefineContent"),
validatePath(pathParts, directiveKind) {
const raw = this.reconstructRawString(pathParts).trim();
let hasVariables = false;
if (pathParts && pathParts.length > 0) {
for (const node of pathParts) {
if (node.type === NodeType.VariableReference) {
hasVariables = true;
}
}
}
const finalFlags = {
hasVariables
};
const result = {
raw,
values: pathParts,
...finalFlags
};
this.debug("PATH", "validatePath final result:", JSON.stringify(result, null, 2));
return result;
},
getImportSubtype(list) {
if (!list) return "importAll";
if (list.length === 0) return "importAll";
if (list.length === 1 && list[0].name === "*") return "importAll";
return "importSelected";
},
trace(pos, reason) {
},
reconstructRawString(nodes) {
if (!Array.isArray(nodes)) {
if (nodes && typeof nodes === "object") {
if (nodes.type === NodeType.Text) return nodes.content || "";
if (nodes.type === NodeType.VariableReference) {
const varId = nodes.identifier;
const valueType = nodes.valueType;
const fields = nodes.fields || [];
let fieldPath = "";
for (const field of fields) {
if (field.type === "field" || field.type === "dot") {
fieldPath += `.${field.name || field.value}`;
} else if (field.type === "array") {
fieldPath += `[${field.index}]`;
}
}
if (valueType === "varInterpolation") {
return `{{${varId}${fieldPath}}}`;
} else if (valueType === "varIdentifier") {
return `@${varId}${fieldPath}`;
} else {
return `{{${varId}${fieldPath}}}`;
}
}
if (nodes.type === "ConditionalStringFragment") {
const conditionRaw = this.reconstructRawString(nodes.condition);
const contentRaw = this.reconstructRawString(nodes.content || []);
return `${conditionRaw}?"${contentRaw}"`;
}
if (nodes.type === "ConditionalTemplateSnippet") {
const conditionRaw = this.reconstructRawString(nodes.condition);
const contentRaw = this.reconstructRawString(nodes.content || []);
return `${conditionRaw}?\`${contentRaw}\``;
}
}
return String(nodes || "");
}
let raw = "";
for (const node of nodes) {
if (!node) continue;
if (node.type === NodeType.Text) {
raw += node.content || "";
} else if (node.type === NodeType.VariableReference) {
const varId = node.identifier;
const valueType = node.valueType;
const fields = node.fields || [];
let fieldPath = "";
for (const field of fields) {
if (field.type === "field" || field.type === "dot") {
fieldPath += `.${field.name || field.value}`;
} else if (field.type === "array") {
fieldPath += `[${field.index}]`;
}
}
if (valueType === "varInterpolation") {
raw += `{{${varId}${fieldPath}}}`;
} else if (valueType === "varIdentifier") {
raw += `@${varId}${fieldPath}`;
} else {
raw += `{{${varId}${fieldPath}}}`;
}
} else if (node.type === NodeType.PathSeparator) {
raw += node.value || "";
} else if (node.type === NodeType.SectionMarker) {
raw += node.value || "";
} else if (node.type === NodeType.StringLiteral) {
raw += node.value || "";
} else if (node.type === "ConditionalStringFragment") {
const conditionRaw = this.reconstructRawString(node.condition);
const contentRaw = this.reconstructRawString(node.content || []);
raw += `${conditionRaw}?"${contentRaw}"`;
} else if (node.type === "ConditionalTemplateSnippet") {
const conditionRaw = this.reconstructRawString(node.condition);
const contentRaw = this.reconstructRawString(node.content || []);
raw += `${conditionRaw}?\`${contentRaw}\``;
} else if (typeof node === "string") {
raw += node;
} else {
raw += node.content || node.value || node.raw || "";
}
}
return raw;
},
createPathMetadata(rawPath, parts) {
return {
hasVariables: parts.some((p) => p && p.type === NodeType.VariableReference),
isAbsolute: rawPath.startsWith("/"),
hasExtension: /\.[a-zA-Z0-9]+$/.test(rawPath),
extension: rawPath.match(/\.([a-zA-Z0-9]+)$/)?.[1] || null
};
},
createCommandMetadata(parts) {
return {
hasVariables: parts.some((p) => p && p.type === NodeType.VariableReference)
};
},
createTemplateMetadata(parts, wrapperType) {
return {
hasVariables: parts.some((p) => p && (p.type === NodeType.VariableReference || p.type === NodeType.ExecInvocation || p.type === "ConditionalTemplateSnippet" || p.type === "ConditionalStringFragment")),
isTemplateContent: wrapperType === "doubleBracket"
};
},
createUrlMetadata(protocol, parts, hasSection = false) {
return {
isUrl: true,
protocol,
hasVariables: parts.some((p) => p && p.type === NodeType.VariableReference),
hasSection
};
},
ttlToSeconds(value, unit) {
const multipliers = {
"milliseconds": 1 / 1e3,
"seconds": 1,
"minutes": 60,
"hours": 3600,
"days": 86400,
"weeks": 604800
};
return value * (multipliers[unit] || 1);
},
detectFormatFromPath(path74) {
const ext = path74.match(/\.([a-zA-Z0-9]+)$/)?.[1]?.toLowerCase();
if (!ext) return null;
const formatMap = {
"json": "json",
"xml": "xml",
"yaml": "yaml",
"yml": "yaml",
"csv": "csv",
"md": "markdown",
"markdown": "markdown",
"txt": "text",
"text": "text"
};
return formatMap[ext] || null;
},
createSectionMeta(pathParts, sectionParts, hasRename) {
return {
sourceType: "section",
hasVariables: [
...pathParts,
...sectionParts
].some((part) => part && part.type === "VariableReference"),
hasRename
};
},
reconstructSectionPath(pathParts, sectionParts) {
const pathStr = this.reconstructRawString(pathParts);
const sectionStr = this.reconstructRawString(sectionParts);
return `${pathStr} # ${sectionStr}`;
},
/**
* Checks if we're at a bracket that should end command parsing
* This uses a specific heuristic: ] at end of input OR ] on its own line
*/
isCommandEndingBracket(input, pos) {
if (input[pos] !== "]") return false;
const nextPos = pos + 1;
if (nextPos >= input.length) return true;
let i = nextPos;
while (i < input.length && (input[i] === " " || input[i] === " ")) {
i++;
}
return i >= input.length || input[i] === "\n";
},
/**
* Parse command content that may contain variables and text segments
* This is used by the CommandBracketContent rule to handle @var interpolation
*
* @param content - The content to parse
* @param baseLocation - The location of the content in the source
*/
parseCommandContent(content, baseLocation) {
const parts = [];
let i = 0;
let currentText = "";
let textStartOffset = 0;
if (!baseLocation) {
console.warn("parseCommandContent called without baseLocation");
return this.parseCommandContentLegacy(content);
}
let currentOffset = baseLocation.start.offset;
let currentLine = baseLocation.start.line;
let currentColumn = baseLocation.start.column;
while (i < content.length) {
if (content[i] === "@" && i + 1 < content.length) {
if (currentText) {
const textEndOffset = currentOffset;
const textEndLine = currentLine;
const textEndColumn = currentColumn;
parts.push(this.createNode(NodeType.Text, {
content: currentText,
location: {
start: {
offset: baseLocation.start.offset + textStartOffset,
line: baseLocation.start.line,
column: baseLocation.start.column + textStartOffset
},
end: {
offset: textEndOffset,
line: textEndLine,
column: textEndColumn
}
}
}));
currentText = "";
}
const varStartOffset = currentOffset;
const varStartLine = currentLine;
const varStartColumn = currentColumn;
i++;
currentOffset++;
currentColumn++;
let varName = "";
while (i < content.length && /[a-zA-Z0-9_]/.test(content[i])) {
varName += content[i];
i++;
currentOffset++;
currentColumn++;
}
if (varName) {
const varEndOffset = currentOffset;
const varEndLine = currentLine;
const varEndColumn = currentColumn;
parts.push(this.createVariableReferenceNode("varIdentifier", {
identifier: varName
}, {
start: {
offset: varStartOffset,
line: varStartLine,
column: varStartColumn
},
end: {
offset: varEndOffset,
line: varEndLine,
column: varEndColumn
}
}));
textStartOffset = i;
} else {
currentText += "@";
}
} else {
if (currentText === "") {
textStartOffset = i;
}
currentText += content[i];
if (content[i] === "\n") {
currentLine++;
currentColumn = 1;
} else {
currentColumn++;
}
currentOffset++;
i++;
}
}
if (currentText) {
parts.push(this.createNode(NodeType.Text, {
content: currentText,
location: {
start: {
offset: baseLocation.start.offset + textStartOffset,
line: baseLocation.start.line,
column: baseLocation.start.column + textStartOffset
},
end: {
offset: currentOffset,
line: currentLine,
column: currentColumn
}
}
}));
}
return parts;
},
/**
* Legacy version of parseCommandContent for backward compatibility
* Creates nodes without proper location data
*/
parseCommandContentLegacy(content) {
const parts = [];
let i = 0;
let currentText = "";
while (i < content.length) {
if (content[i] === "@" && i + 1 < content.length) {
if (currentText) {
parts.push(this.createNode(NodeType.Text, {
content: currentText
}));
currentText = "";
}
i++;
let varName = "";
while (i < content.length && /[a-zA-Z0-9_]/.test(content[i])) {
varName += content[i];
i++;
}
if (varName) {
parts.push(this.createNode(NodeType.Text, {
content: "@" + varName
}));
} else {
currentText += "@";
}
} else {
currentText += content[i];
i++;
}
}
if (currentText) {
parts.push(this.createNode(NodeType.Text, {
content: currentText
}));
}
return parts;
},
/**
* Create an ExecInvocation node
*/
createExecInvocation(commandRef, withClause, location) {
return this.createNode("ExecInvocation", {
commandRef,
withClause: withClause || null,
location
});
},
attachPostFields(exec2, post) {
if (!post || post.length === 0) {
return exec2;
}
let current = exec2;
const tail = current.withClause || null;
if (tail) {
current = {
...current,
withClause: null
};
}
const additionalFields = [];
for (const entry of post) {
if (entry?.type === "methodCall") {
if (additionalFields.length > 0) {
const existingFields = current.fields || [];
current = {
...current,
fields: [
...existingFields,
...additionalFields
]
};
additionalFields.length = 0;
}
const methodRef = {
name: entry.name,
identifier: [
this.createNode(NodeType.Text, {
content: entry.name,
location: entry.location
})
],
args: entry.args || [],
isCommandReference: true,
objectSource: current
};
current = this.createExecInvocation(methodRef, null, entry.location);
} else {
additionalFields.push(entry);
}
}
if (additionalFields.length > 0) {
const existingFields = current.fields || [];
current = {
...current,
fields: [
...existingFields,
...additionalFields
]
};
}
if (tail) {
current = {
...current,
withClause: tail
};
}
return current;
},
applyTail(exec2, tail) {
if (!tail) {
return exec2;
}
return {
...exec2,
withClause: tail
};
},
/**
* Get the command name from an ExecInvocation node
*/
getExecInvocationName(node) {
if (!node || node.type !== "ExecInvocation") return null;
return node.commandRef?.identifier || node.commandRef?.name;
},
/**
* Check if a node is an ExecInvocation
*/
isExecInvocationNode(node) {
return node?.type === "ExecInvocation";
},
/**
* Parse a JavaScript code block using acorn to find the complete block
* This handles nested braces, strings, template literals, etc. properly
*
* @param input - The full input string
* @param startPos - Position after the opening brace
* @returns The parsed code content and end position, or null if invalid
*/
parseJavaScriptBlock(input, startPos) {
const potentialCode = input.substring(startPos);
let lastValidEnd = -1;
let lastValidCode = "";
for (let i = 0; i < potentialCode.length; i++) {
if (potentialCode[i] !== "}") continue;
const testCode = potentialCode.substring(0, i);
try {
acorn.parse(`(${testCode})`, {
ecmaVersion: "latest",
allowReturnOutsideFunction: true
});
lastValidEnd = i;
lastValidCode = testCode;
} catch (e) {
try {
acorn.parse(testCode, {
ecmaVersion: "latest",
allowReturnOutsideFunction: true,
sourceType: "module"
});
lastValidEnd = i;
lastValidCode = testCode;
} catch (e2) {
}
}
}
if (lastValidEnd >= 0) {
return {
content: lastValidCode.trim(),
endPos: startPos + lastValidEnd
};
}
return null;
},
// Array vs Path disambiguation helpers for /var directive
createEmptyArray(location) {
return {
type: "array",
items: [],
location
};
},
createArrayFromContent(content, location) {
return {
type: "array",
items: content,
location
};
},
createSectionExtraction(content, location) {
return {
type: "section",
path: content.path,
section: content.section,
location
};
},
createPathDereference(content, location) {
return {
type: "path",
segments: content,
location
};
},
createObjectFromProperties(properties, location) {
return {
type: "object",
properties: properties || {},
location
};
},
// Error Recovery Helper Functions
// --------------------------------
/**
* Checks if an array is unclosed by scanning ahead
* Returns true if we hit a newline before finding the closing bracket
*/
isUnclosedArray(input, pos) {
let depth = 1;
let i = pos;
let hasHash = false;
this.debug("isUnclosedArray starting at pos", pos, "first 50 chars:", input.substring(pos, pos + 50));
while (i < input.length && depth > 0) {
const char = input[i];
if (char === "[") {
depth++;
this.debug("Found [ at", i, "depth now", depth);
} else if (char === "]") {
depth--;
this.debug("Found ] at", i, "depth now", depth);
} else if (char === "#" && depth === 1) {
hasHash = true;
this.debug("Found # at", i, "in brackets - this is section syntax");
} else if (char === "\n" && depth > 0) {
if (!hasHash) {
this.debug("Found newline at", i, "without # - unclosed array");
return true;
}
this.debug("Found newline at", i, "but has # - continuing scan");
}
i++;
}
const result = depth > 0;
this.debug("isUnclosedArray finished: result=", result, "hasHash=", hasHash, "depth=", depth, "scanned to pos", i);
return result;
},
/**
* Checks if an object is unclosed by scanning ahead
* Returns true if we hit a newline before finding the closing brace
*/
isUnclosedObject(input, pos) {
let depth = 1;
let i = pos;
let inString = false;
let stringChar = null;
while (i < input.length && depth > 0) {
const char = input[i];
if ((char === '"' || char === "'") && (i === 0 || input[i - 1] !== "\\")) {
if (!inString) {
inString = true;
stringChar = char;
} else if (char === stringChar) {
inString = false;
stringChar = null;
}
}
if (!inString) {
if (char === "{") depth++;
else if (char === "}") depth--;
else if (char === "\n" && depth > 0) return true;
}
i++;
}
return depth > 0;
},
/**
* Checks if a string quote is unclosed
* Returns true if we hit a newline or end of input before finding the closing quote
*/
detectMissingQuoteClose(input, pos, quoteChar) {
let i = pos;
while (i < input.length) {
if (input[i] === quoteChar && input[i - 1] !== "\\") return false;
if (input[i] === "\n") return true;
i++;
}
return true;
},
/**
* Checks if a template delimiter (::) is unclosed
*/
isUnclosedTemplate(input, pos) {
let i = pos;
while (i < input.length - 1) {
if (input[i] === ":" && input[i + 1] === ":") return false;
i++;
}
return true;
},
/**
* Checks if we're at the start of what looks like a multiline array
* (array with newline after opening bracket)
*/
isMultilineArrayStart(input, pos) {
let i = pos;
while (i < input.length && (input[i] === " " || input[i] === " ")) {
i++;
}
return i < input.length && input[i] === "\n";
},
/**
* Scans ahead to check if this looks like a valid language identifier for /run
*/
isValidLanguageKeyword(input, pos, lang) {
const validLanguages = [
"js",
"javascript",
"node",
"python",
"py",
"bash",
"sh"
];
return validLanguages.includes(lang.toLowerCase());
},
/**
* Checks if we're missing a 'from' keyword in an import statement
*/
isMissingFromKeyword(input, pos) {
let i = pos;
while (i < input.length && (input[i] === " " || input[i] === " ")) {
i++;
}
if (i < input.length) {
const char = input[i];
return char === '"' || char === "'" || char === "[" || char === "@";
}
return false;
},
/**
* Create an error with enhanced location tracking
* Since we can't access parser internals from here, we'll just throw
* a regular error and let the parser enhance it
*/
mlldError(message, expectedToken, loc) {
const error = new Error(message);
error.isMlldError = true;
error.expectedToken = expectedToken;
error.mlldErrorLocation = loc;
throw error;
},
/**
* Capture content inside balanced [ ] brackets starting at startPos (the first character after '[')
* Returns null if no matching closing bracket is found
*/
captureBracketContent(input, startPos) {
let depth = 1;
let i = startPos;
let inString = false;
let quote2 = null;
while (i < input.length && depth > 0) {
const ch = input[i];
if (inString) {
if (ch === quote2 && input[i - 1] !== "\\") {
inString = false;
quote2 = null;
}
} else {
if (ch === '"' || ch === "'" || ch === "`") {
inString = true;
quote2 = ch;
} else if (ch === "[") {
depth++;
} else if (ch === "]") {
depth--;
if (depth === 0) {
return {
content: input.slice(startPos, i),
endOffset: i
};
}
}
}
i++;
}
return null;
},
/**
* Offset a location object by a base location (start of the block content)
*/
offsetLocation(loc, baseLocation) {
if (!loc || !baseLocation?.start) return loc;
const baseStart = baseLocation.start;
const adjustPosition = /* @__PURE__ */ __name((pos) => {
const line = (pos?.line || 1) + (baseStart.line || 1) - 1;
const column = pos?.line === 1 ? (pos?.column || 1) + (baseStart.column || 1) - 1 : pos?.column || 1;
return {
offset: (pos?.offset || 0) + (baseStart.offset || 0),
line,
column
};
}, "adjustPosition");
return {
source: baseLocation.source || loc.source,
start: adjustPosition(loc.start),
end: adjustPosition(loc.end)
};
},
/**
* Reparse a block substring with a specific start rule to surface inner errors with corrected offsets
*/
reparseBlock(options) {
const parseOptions = {
startRule: options.startRule
};
if (options.mode) parseOptions.mode = options.mode;
if (options.grammarSource) parseOptions.grammarSource = options.grammarSource;
try {
const normalizedText = options.text.replace(/\s+$/, "");
options.parse(normalizedText, parseOptions);
} catch (error) {
const err = error;
if (err instanceof options.SyntaxErrorClass && err.location) {
const adjustedLocation = this.offsetLocation(err.location, options.baseLocation);
const enhancedError = new options.SyntaxErrorClass(err.message, err.expected, err.found, adjustedLocation);
enhancedError.expected = err.expected;
enhancedError.found = err.found;
enhancedError.location = adjustedLocation;
throw enhancedError;
}
throw error;
}
throw this.mlldError("Invalid block content.", void 0, options.baseLocation);
},
// Parser State Management for Code Blocks
// ----------------------------------------
// These functions help prevent state corruption when parsing multiple
// complex functions in mlld-run blocks
/**
* Parser state tracking object
* Used to detect and prevent state corruption issues
*/
parserState: {
codeBlockDepth: 0,
braceDepth: 0,
inString: false,
stringChar: null,
lastDirectiveEndPos: -1,
functionCount: 0,
maxNestingDepth: 20
},
/**
* Reset parser state between functions
* This prevents state corruption when parsing multiple complex functions
*/
resetCodeParsingState() {
this.parserState.braceDepth = 0;
this.parserState.inString = false;
this.parserState.stringChar = null;
this.parserState.functionCount++;
this.debug("Parser state reset", {
functionCount: this.parserState.functionCount,
lastEndPos: this.parserState.lastDirectiveEndPos
});
},
/**
* Get current brace depth for debugging and limits
*/
getBraceDepth() {
return this.parserState.braceDepth;
},
/**
* Increment brace depth with overflow checking
*/
incrementBraceDepth() {
this.parserState.braceDepth++;
if (this.parserState.braceDepth > this.parserState.maxNestingDepth) {
this.mlldError(`Code block nesting too deep (${this.parserState.braceDepth} levels). Consider simplifying your function or splitting it into smaller functions.`);
}
},
/**
* Decrement brace depth with underflow checking
*/
decrementBraceDepth() {
this.parserState.braceDepth--;
if (this.parserState.braceDepth < 0) {
this.debug("WARNING: Brace depth underflow detected", {
depth: this.parserState.braceDepth,
functionCount: this.parserState.functionCount
});
this.parserState.braceDepth = 0;
}
},
/**
* Validate parser state consistency
* Returns true if state is valid, false if corrupted
*/
validateParserState() {
const isValid = this.parserState.braceDepth >= 0 && this.parserState.braceDepth <= this.parserState.maxNestingDepth;
if (!isValid) {
this.debug("Parser state validation failed", {
braceDepth: this.parserState.braceDepth,
inString: this.parserState.inString,
functionCount: this.parserState.functionCount
});
}
return isValid;
},
/**
* Mark the end of a directive for state tracking
*/
markDirectiveEnd(pos) {
this.parserState.lastDirectiveEndPos = pos;
},
// File Reference Helper Functions
// --------------------------------
/**
* Checks if content inside <...> represents a file reference
* File references are detected by presence of: . * @
* Note: We don't include / since we don't support directories
* Files without extensions can be used outside interpolation contexts
*/
isFileReferenceContent(content) {
if (content.trim().startsWith("!")) {
return false;
}
return /[.*@]/.test(content);
},
/**
* Creates a FileReference AST node
*/
createFileReferenceNode(source, fields, pipes, location) {
return {
type: "FileReference",
nodeId: (0, import_crypto.randomUUID)(),
source,
fields: fields || [],
pipes: pipes || [],
location,
meta: {
isFileReference: true,
hasGlob: typeof source === "object" && source.raw && source.raw.includes("*"),
isPlaceholder: source && source.type === "placeholder"
}
};
},
// Binary expression builder with left-to-right associativity
createBinaryExpression(first, rest, location) {
if (!rest || rest.length === 0) return first;
return rest.reduce((left, { op, right }) => this.createNode("BinaryExpression", {
operator: op,
left,
right,
location
}), first);
},
buildWhenBoundPatternExpression(boundIdentifier, pattern) {
const anchorLocation = /* @__PURE__ */ __name((loc) => {
if (!loc || !loc.start) return loc;
return {
start: loc.start,
end: loc.start
};
}, "anchorLocation");
const boundRef = /* @__PURE__ */ __name((loc) => this.createVariableReferenceNode("identifier", {
identifier: boundIdentifier
}, anchorLocation(loc)), "boundRef");
const build = /* @__PURE__ */ __name((p) => {
if (!p) return p;
if (p.kind === "logical") {
const first = build(p.first);
const rest = Array.isArray(p.rest) ? p.rest.map((r) => ({
op: r.op,
right: build(r.right)
})) : [];
return this.createBinaryExpression(first, rest, p.location);
}
if (p.kind === "wildcard") return p.node;
if (p.kind === "compare") {
return this.createNode("BinaryExpression", {
operator: p.op,
left: boundRef(p.location),
right: p.right,
location: p.location
});
}
if (p.kind === "equals") {
const value = p.value;
if (value && typeof value === "object" && "type" in value && value.type === "Literal") {
if (value.valueType === "none" || value.valueType === "wildcard") return value;
}
return this.createNode("BinaryExpression", {
operator: "==",
left: boundRef(p.location),
right: value,
location: p.location
});
}
return p;
}, "build");
return build(pattern);
},
// Check if nodes contain newlines
containsNewline(nodes) {
if (!Array.isArray(nodes)) nodes = [
nodes
];
return nodes.some((n) => n.type === "Newline" || n.content && n.content.includes("\n") || n.raw && n.raw.includes("\n"));
},
/**
* Creates a WhenExpression node for when expressions (used in /var assignments)
*/
createWhenExpression(conditions, withClause, location, modifier = null, bound = null) {
return this.createNode(NodeType.WhenExpression, {
conditions,
withClause: withClause || null,
...bound ? {
boundIdentifier: bound.boundIdentifier,
boundValue: bound.boundValue
} : {},
meta: {
conditionCount: conditions.length,
isValueReturning: true,
evaluationType: "expression",
hasTailModifiers: !!withClause,
modifier,
hasBoundValue: !!bound,
...bound ? {
boundIdentifier: bound.boundIdentifier
} : {}
},
location
});
},
/**
* Creates a ForExpression node for for...in expressions in /var assignments
*/
createForExpression(variable, source, expression, location, opts, batchPipeline) {
const meta = {
isForExpression: true
};
if (opts) {
meta.forOptions = opts;
}
if (batchPipeline) {
meta.batchPipeline = batchPipeline;
}
return {
type: "ForExpression",
nodeId: (0, import_crypto.randomUUID)(),
variable,
source,
expression: Array.isArray(expression) ? expression : [
expression
],
location,
meta
};
},
/**
* Creates an action node for /for directive actions
*/
createForActionNode(directive, content, location, endingTail, endingComment) {
const kind = directive;
if (kind === "show" && content) {
if (content && typeof content === "object" && "content" in content && "wrapperType" in content) {
const values2 = {
content: content.content
};
if (endingTail && endingTail.pipeline) {
values2.pipeline = endingTail.pipeline;
}
const meta3 = {
implicit: false,
isTemplateContent: true
};
if (endingComment) {
meta3.comment = endingComment;
}
return [
this.createNode(NodeType.Directive, {
kind,
subtype: "showTemplate",
values: values2,
raw: {
content: this.reconstructRawString(content.content)
},
meta: meta3,
location
})
];
}
const isExec = content && typeof content === "object" && content.type === "ExecInvocation";
const values = {
invocation: content
};
if (endingTail && endingTail.pipeline) {
values.withClause = {
pipeline: endingTail.pipeline
};
}
const meta2 = {
implicit: false
};
if (endingComment) {
meta2.comment = endingComment;
}
return [
this.createNode(NodeType.Directive, {
kind,
subtype: isExec ? "showInvocation" : "showVariable",
values,
raw: {
content: this.reconstructRawString(content)
},
meta: meta2,
location
})
];
}
const meta = {
implicit: false
};
if (endingComment) {
meta.comment = endingComment;
}
return [
this.createNode(NodeType.Directive, {
kind,
subtype: kind,
values: {
content: Array.isArray(content) ? content : [
content
]
},
raw: {
content: this.reconstructRawString(content)
},
meta,
location
})
];
},
/**
* Helper functions for expression context detection
*/
isSimpleCondition(expr) {
return expr.type === "VariableReference" || expr.type === "Literal" || expr.type === "UnaryExpression" && expr.operator === "!";
},
extractConditionVariables(expr) {
const variables = [];
function traverse(node) {
if (node.type === "VariableReference") {
variables.push(node.name || node.identifier);
} else if (node.left) traverse(node.left);
if (node.right) traverse(node.right);
if (node.operand) traverse(node.operand);
}
__name(traverse, "traverse");
traverse(expr);
return [
...new Set(variables)
];
},
/**
* Unified pipeline processing helper
* Consolidates pipeline ha