Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 133 additions & 3 deletions docs/architecture/rfcs/semantic-vocabulary-convergence-v0.md

Large diffs are not rendered by default.

101 changes: 98 additions & 3 deletions docs/architecture/rfcs/semantic-vocabulary-convergence-v0.zh-CN.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion loopx/semantics/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ def _typescript_scan(
and isinstance(error.get('line'), int) and error['line'] > 0):
raise ValueError(f"{error['path']}:{error['line']}: invalid TypeScript source; repair syntax before semantic scanning")
raise ValueError('TypeScript production parser failed; run npm ci --ignore-scripts and check the Node runtime')
# The parser names the same blocker vocabulary as the Python scanner;
# ``typescript_dynamic`` stays the fallback for a form it cannot classify.
rows.extend(Production(r['site'], r['line'], r['form'], frozenset(r['values']), r['unresolved'],
'typescript_dynamic' if r['unresolved'] else None)
(r.get('blocker') or 'typescript_dynamic') if r['unresolved'] else None)
for r in json.loads(completed.stdout))
return rows

Expand Down
480 changes: 387 additions & 93 deletions loopx/semantics/python_production.py

Large diffs are not rendered by default.

41 changes: 35 additions & 6 deletions scripts/semantic_production_scan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,48 @@ for (const source of request.sources) {
const field = request.field;
const returns = new Set((request.return_functions ?? []).filter(x => x.startsWith(`${source.path}::`)));
const unwrap = node => {
while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node))) node = node.expression;
while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) ||
ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
return node;
};
// Say why a write stayed unknown using the same labels as the Python scanner,
// so one residue taxonomy covers both runtimes instead of a single catch-all.
const blockerFor = node => {
if (!node) return 'other';
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) return 'attribute_read';
if (ts.isCallExpression(node) || ts.isNewExpression(node) || ts.isAwaitExpression(node)) return 'call_result';
if (ts.isIdentifier(node)) return 'unstable_local';
// `other`, not `dynamic_key`: the shared vocabulary defines `dynamic_key`
// as a computed or non-literal subscript, and none of these is one. Python
// answers `other` for the same shapes -- a dict literal or an f-string
// where a scalar was required -- so the two runtimes agree on the label.
if (ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node) ||
ts.isTemplateExpression(node)) return 'other';
return 'typescript_dynamic';
};
const merge = parts => ({
values: [...new Set(parts.flatMap(part => part.values))].sort(),
unresolved: parts.some(part => part.unresolved),
blocker: parts.find(part => part.unresolved)?.blocker,
});
const values = expression => {
const node = unwrap(expression);
if (!node) return {values: [], unresolved: true};
if (!node) return {values: [], unresolved: true, blocker: 'other'};
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return {values: node.text ? [node.text] : [], unresolved: false};
if (node.kind === ts.SyntaxKind.NullKeyword) return {values: [], unresolved: false};
if (ts.isConditionalExpression(node)) {
const left = values(node.whenTrue), right = values(node.whenFalse);
return {values: [...new Set([...left.values, ...right.values])].sort(), unresolved: left.unresolved || right.unresolved};
// ``undefined`` carries no value and is not an unknown, matching the way
// the Python scanner treats an explicit ``None``.
if (ts.isIdentifier(node) && node.text === 'undefined') return {values: [], unresolved: false};
if (ts.isConditionalExpression(node)) return merge([values(node.whenTrue), values(node.whenFalse)]);
// ``a || b`` and ``a ?? b`` are a finite selection, exactly like the
// Python scanner's BoolOp arms; ``String(x)`` is a transparent wrapper.
if (ts.isBinaryExpression(node) && [ts.SyntaxKind.BarBarToken,
ts.SyntaxKind.QuestionQuestionToken].includes(node.operatorToken.kind)) {
return merge([values(node.left), values(node.right)]);
}
return {values: [], unresolved: true};
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) &&
node.expression.text === 'String' && node.arguments.length === 1) return values(node.arguments[0]);
return {values: [], unresolved: true, blocker: blockerFor(node)};
};
const staticName = expression => {
const node = unwrap(expression);
Expand Down
Loading
Loading