- 받는 상자는 들어오는 선이 모두 쉬는 갈래일 때만 쉼 · 하나라도 살아 있으면 셈(쉬는 갈래 값은 빈 칸) · 쉬는 선은 쉼 줄만 - 조건 선 바로 뒤 상자는 그대로 막음 · M02_DF_Compute.py 도 같은 뜻(resting · arrive) - 시험: 지반 견본 토사 → 1㎥ 경비 2,000 · 암 → 브레이커 갈래 값 · 쉼 줄 · 문제 0 · 서버 = 화면(26개) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
476 lines
20 KiB
TypeScript
476 lines
20 KiB
TypeScript
/* =============================================================================
|
|
* M02_DF_Eval.ts
|
|
* 단가산출 흐름도 계산 · 검사 — 상자를 기댐 차례대로(입력 → 선 규칙 → 함수 · 다른 로직 → 출력) 셈
|
|
* 함수 상자 = 시트를 우리 스프레드시트 엔진으로 셈(받는 칸에 앞 값) · 선 규칙 · 끝수도 같은 엔진(`M02_DF_Rule`)
|
|
* 출력 상자를 비용 종류별로 모아 원가 · 끊긴 곳(빈 받는 자리 · 통과 안 됨 · 표에 없는 조건 · 맞는 재료 없음 ·
|
|
* 조건 겹침 · 조건 안 맞음 · 오류) · DOM 없음
|
|
* 엔진을 정적으로 가져옴 — 화면(Entry)은 DataFlow 를 import() 로 늦게 불러 첫 묶음에 안 들어가게
|
|
* ========================================================================== */
|
|
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import { parseA1, toA1 } from "../../A00_Common/spreadsheet/spreadsheet_address";
|
|
import { recalcWorkbook } from "../../A00_Common/spreadsheet/spreadsheet_recalc";
|
|
import type { CalcValue, Cell, Sheet } from "../../A00_Common/spreadsheet/spreadsheet_types";
|
|
import {
|
|
OUTPUT_PORT,
|
|
depends,
|
|
designInputs,
|
|
elementPort,
|
|
portsIn,
|
|
portsOut,
|
|
ruleSources,
|
|
type CfBox,
|
|
type CfLink,
|
|
type CfLogic,
|
|
} from "./M02_DF_Model";
|
|
import { applyRound, applyRule, ruleLabel } from "./M02_DF_Rule";
|
|
|
|
/** 알약 값 — 수 · 글 · 참거짓 · 빈 값 · 표(머리 줄 포함 칸 덩어리) */
|
|
export type CfValue = number | string | boolean | null | (number | string | null)[][];
|
|
|
|
/** 입력 값 묶음 — 상자 id → 알약 이름 → 값(입력 상자 요소 값 · 표 · 변수 값 · 함수 상자 변수 값 · 다른 로직 출력) */
|
|
export type CfValues = Record<string, Record<string, CfValue>>;
|
|
|
|
type Scalar = number | string | boolean | null;
|
|
|
|
/** 끊긴 곳 하나 — 상자 · 알약 · 선 위에 표시할 것 */
|
|
export interface CfBreak {
|
|
kind:
|
|
| "빈 받는 자리"
|
|
| "통과 안 됨"
|
|
| "표에 없는 조건"
|
|
| "맞는 재료 없음"
|
|
| "조건 겹침"
|
|
| "조건 안 맞음"
|
|
| "오류";
|
|
/** 무게 — 「쉼」 = 조건이 안 맞아 쉬는 갈래(그 아래로 번진 빈 값 포함) · 「문제」 = 고쳐야 할 끊김 */
|
|
level: "문제" | "쉼";
|
|
box: string;
|
|
port?: string;
|
|
link?: string;
|
|
/** 쉬운 말 */
|
|
text: string;
|
|
}
|
|
|
|
export interface CfEvalResult {
|
|
/** 상자 id → 내보내는 알약 이름 → 값 */
|
|
ports: Record<string, Record<string, CfValue>>;
|
|
/** 상자 id → 받는 알약 이름 → 선 규칙을 지나 들어온 값 */
|
|
arrived?: Record<string, Record<string, CfValue>>;
|
|
/** 출력 상자 id → 값 */
|
|
outputs: Record<string, Scalar>;
|
|
cost: {
|
|
노무비: number;
|
|
재료비: number;
|
|
경비: number;
|
|
/** 비용 아닌 출력(값) — 출력 상자 이름 → 값 */
|
|
값들: Record<string, Scalar>;
|
|
계: number;
|
|
};
|
|
/** 쉬운 말 */
|
|
errors: string[];
|
|
/** 끊긴 곳(셈 때 생긴 것) */
|
|
breaks?: CfBreak[];
|
|
}
|
|
|
|
/** 다른 로직 입력을 셀 때 쓸 로직들(저장 · 견본 key → 로직) */
|
|
export interface CfEvalOptions {
|
|
logics?: Record<string, CfLogic>;
|
|
depth?: number;
|
|
}
|
|
|
|
const TEXT = {
|
|
/** {box} {cell} {err} */
|
|
CellError: [
|
|
"「{box}」 {cell} 칸 계산이 막힘 — {err}",
|
|
"「{box}」 cell {cell} can't be worked out — {err}",
|
|
],
|
|
Cycle: ["돌고 도는 참조", "circular reference"],
|
|
/** {box} */
|
|
Loop: ["「{box}」 이 돌고 도는 선에 걸림", "「{box}」 is in a loop of lines"],
|
|
NotNumber: ["「{box}」 에 숫자가 아닌 값이 들어옴", "「{box}」 got a value that isn't a number"],
|
|
/** {box} {pill} */
|
|
EmptyIn: [
|
|
"「{box}」 의 받는 알약 「{pill}」 에 선이 없음",
|
|
"Pill 「{pill}」 of 「{box}」 has no line",
|
|
],
|
|
NoCell: ["「{box}」 의 출력 칸이 정해지지 않음", "Output cell of 「{box}」 is not set"],
|
|
NoOutput: ["출력 상자가 없음", "There is no output box"],
|
|
NoInCell: [
|
|
"「{box}」 의 받는 알약 「{pill}」 을 칸에 놓지 않음",
|
|
"Pill 「{pill}」 of 「{box}」 is not placed in a cell",
|
|
],
|
|
NoValue: ["「{box}」 의 「{pill}」 값이 아직 없음", "「{pill}」 of 「{box}」 has no value yet"],
|
|
/** {from} {box} {rule} */
|
|
NotPassed: [
|
|
"「{from}」 → 「{box}」 선이 조건({rule})을 통과하지 않음",
|
|
"Line 「{from}」 → 「{box}」 doesn't pass ({rule})",
|
|
],
|
|
NoRow: [
|
|
"「{from}」 → 「{box}」 선 — 표에 맞는 줄이 없음",
|
|
"Line 「{from}」 → 「{box}」 — no matching table row",
|
|
],
|
|
ManyRows: [
|
|
"「{from}」 → 「{box}」 선 — 표에 맞는 줄이 여럿",
|
|
"Line 「{from}」 → 「{box}」 — several table rows match",
|
|
],
|
|
/** {from} {box} */
|
|
Rest: [
|
|
"「{from}」 → 「{box}」 — 앞 조건이 안 맞아 쉬는 갈래",
|
|
"「{from}」 → 「{box}」 — resting branch (an earlier condition doesn't match)",
|
|
],
|
|
RuleError: [
|
|
"「{from}」 → 「{box}」 선 규칙을 셀 수 없음",
|
|
"Line 「{from}」 → 「{box}」 rule can't be worked out",
|
|
],
|
|
/** {box} {pill} */
|
|
NoMaterial: [
|
|
"「{box}」 의 「{pill}」 — 조건에 맞는 재료가 없음",
|
|
"「{pill}」 of 「{box}」 — no material matches",
|
|
],
|
|
Overlap: [
|
|
"「{box}」 의 「{pill}」 에서 나간 조건 선이 둘 이상 통과함",
|
|
"Several condition lines from 「{pill}」 of 「{box}」 pass",
|
|
],
|
|
NoneMatch: [
|
|
"「{box}」 의 「{pill}」 에서 나간 조건 선이 하나도 통과하지 않음",
|
|
"No condition line from 「{pill}」 of 「{box}」 passes",
|
|
],
|
|
NoLogic: [
|
|
"「{box}」 이 부르는 로직을 찾을 수 없음",
|
|
"The logic used by 「{box}」 can't be found",
|
|
],
|
|
} as const;
|
|
|
|
const tl = (key: keyof typeof TEXT, fill: Record<string, string> = {}): string =>
|
|
Object.entries(fill).reduce(
|
|
(s, [k, v]) => s.replace(`{${k}}`, v),
|
|
TEXT[key][currentLanguageIndex === 1 ? 1 : 0] as string,
|
|
);
|
|
|
|
/* ── 차례 ───────────────────────────────────────────────────────────────── */
|
|
|
|
/** 상자 차례 — 기댐(선 · 규칙 조건 · 재료 조건)대로 · 돌고 도는 상자는 loop */
|
|
function order(lg: CfLogic): { boxes: CfBox[]; loop: CfBox[] } {
|
|
const deps = depends(lg);
|
|
const done: CfBox[] = [];
|
|
const left = new Set(lg.boxes.map((b) => b.id));
|
|
for (let moved = true; moved && left.size;) {
|
|
moved = false;
|
|
for (const b of lg.boxes) {
|
|
if (!left.has(b.id)) continue;
|
|
if (deps.some((d) => d.to === b.id && d.from !== b.id && left.has(d.from))) continue;
|
|
done.push(b);
|
|
left.delete(b.id);
|
|
moved = true;
|
|
}
|
|
}
|
|
return { boxes: done, loop: lg.boxes.filter((b) => left.has(b.id)) };
|
|
}
|
|
|
|
/* ── 칸 ─────────────────────────────────────────────────────────────────── */
|
|
|
|
function putValue(칸: Record<string, Cell>, cell: string, value: Scalar): void {
|
|
const style = 칸[cell]?.서식;
|
|
if (value === null) {
|
|
if (style) 칸[cell] = { 서식: style };
|
|
else delete 칸[cell];
|
|
} else 칸[cell] = { 값: value, ...(style ? { 서식: style } : {}) };
|
|
}
|
|
|
|
/** 칸에 값 넣기 — 표는 cell 을 왼위로 펼침 */
|
|
function put(칸: Record<string, Cell>, cell: string, value: CfValue): void {
|
|
const at = parseA1(cell);
|
|
if (!at) return;
|
|
if (!Array.isArray(value)) return putValue(칸, toA1(at.r, at.c), value);
|
|
value.forEach((line, dr) => line.forEach((x, dc) => putValue(칸, toA1(at.r + dr, at.c + dc), x)));
|
|
}
|
|
|
|
const fromCalc = (v: CalcValue | undefined): Scalar => {
|
|
if (!v) return null;
|
|
if ("수" in v) return Number(v.수);
|
|
if ("글" in v) return v.글;
|
|
if ("참" in v) return v.참;
|
|
return v.오류;
|
|
};
|
|
|
|
/* ── 상자마다 ───────────────────────────────────────────────────────────── */
|
|
|
|
/** 입력 상자 알약 값 — 넘긴 값 · 없으면 변수 예시값 · 사용자 요소 값 */
|
|
function inputPorts(
|
|
lg: CfLogic,
|
|
box: CfBox,
|
|
given: Record<string, CfValue>,
|
|
): Record<string, CfValue> {
|
|
const out: Record<string, CfValue> = {};
|
|
const inp = box.input;
|
|
if (!inp) return out;
|
|
const pills = portsOut(box);
|
|
const sources: CfValue[] = [
|
|
...(inp.mode === "group"
|
|
? inp.elements.map((e) => lg.userElements.find((u) => u.id === e.ref)?.value ?? null)
|
|
: []),
|
|
...(inp.mode === "group" && inp.table ? [null] : []),
|
|
...inp.variables.map((v) => v.example),
|
|
];
|
|
pills.forEach((p, i) => (out[p.name] = p.name in given ? given[p.name] : (sources[i] ?? null)));
|
|
return out;
|
|
}
|
|
|
|
/** 함수 상자 하나 — 받는 칸 · 변수 칸에 값을 넣고 시트 하나를 셈 · 출력 끝수 */
|
|
function runFunc(
|
|
box: CfBox,
|
|
arrive: Record<string, CfValue>,
|
|
given: Record<string, CfValue>,
|
|
errors: string[],
|
|
): Scalar {
|
|
const func = box.func!;
|
|
const sheet: Sheet = structuredClone(func.sheet);
|
|
for (const i of func.inputs) put(sheet.칸, i.cell, arrive[i.port] ?? given[i.port] ?? null);
|
|
for (const v of func.variables)
|
|
if (v.cell) put(sheet.칸, v.cell, v.name in given ? given[v.name] : v.example);
|
|
const values = recalcWorkbook({ 종류: "통합문서", 판: 1, 열: "DF", 서식: [{}], 시트: [sheet] });
|
|
for (const [cell, v] of Object.entries(values[sheet.id] ?? {}))
|
|
if ("오류" in v) {
|
|
const err = v.오류 === "#CYCLE!" ? `${v.오류} ${tl("Cycle")}` : v.오류;
|
|
errors.push(tl("CellError", { box: box.name, cell, err }));
|
|
}
|
|
const at = func.output.cell.toUpperCase();
|
|
const c = sheet.칸[at];
|
|
const got = !c ? null : c.식 !== undefined ? fromCalc(values[sheet.id]?.[at]) : (c.값 ?? null);
|
|
return typeof got === "number" && func.output.round ? applyRound(func.output.round, got) : got;
|
|
}
|
|
|
|
/* ── 계산 ───────────────────────────────────────────────────────────────── */
|
|
|
|
/** 화면 계산 — opts.logics 가 있으면 다른 로직 입력을 그 로직으로 셈(없으면 values 의 값) */
|
|
export function evalLogic(
|
|
lg: CfLogic,
|
|
values: CfValues = {},
|
|
opts: CfEvalOptions = {},
|
|
): CfEvalResult {
|
|
const errors: string[] = [];
|
|
const found: CfBreak[] = [];
|
|
const ports: CfEvalResult["ports"] = {};
|
|
const arrived: NonNullable<CfEvalResult["arrived"]> = {};
|
|
const name = (id: string): string => lg.boxes.find((b) => b.id === id)?.name ?? id;
|
|
const valueOf = (box: string, pill: string): CfValue => ports[box]?.[pill] ?? null;
|
|
const passed = new Map<string, boolean>();
|
|
/** 선 규칙에서 막힌 상자 — 그 함수는 셈하지 않고 빈 값을 넘김(갈래가 끊김) */
|
|
const blocked = new Set<string>();
|
|
/** 조건이 안 맞아 쉬는 상자 — 그 아래로 번짐 */
|
|
const resting = new Set<string>();
|
|
/** 선 하나로 들어오는 값 — 규칙이 있으면 엔진으로 */
|
|
const through = (l: CfLink): CfValue => {
|
|
const v = valueOf(l.from.box, l.from.name);
|
|
if (!l.rule) return v;
|
|
if (v === null && (l.rule.kind === "scale" || l.rule.kind === "round")) return null;
|
|
const conds = Object.fromEntries(
|
|
ruleSources(l.rule).map((p) => [p.name, valueOf(p.box, p.name)]),
|
|
);
|
|
const res = applyRule(l.rule, v, conds);
|
|
if (l.rule.kind === "gate") passed.set(l.id, res.why === null);
|
|
const fill = { from: name(l.from.box), box: name(l.to.box), rule: ruleLabel(l.rule) };
|
|
if (res.why === "통과 안 됨") {
|
|
resting.add(l.to.box);
|
|
found.push({
|
|
kind: "통과 안 됨",
|
|
level: "쉼",
|
|
box: l.to.box,
|
|
port: l.to.name,
|
|
link: l.id,
|
|
text: tl("NotPassed", fill),
|
|
});
|
|
} else if (res.why) {
|
|
const kind = res.why === "표에 없는 조건" ? "표에 없는 조건" : "오류";
|
|
const key =
|
|
res.why === "표에 없는 조건"
|
|
? "NoRow"
|
|
: res.why === "표에 여럿 맞음"
|
|
? "ManyRows"
|
|
: "RuleError";
|
|
const text = tl(key, fill);
|
|
found.push({ kind, level: "문제", box: l.to.box, port: l.to.name, link: l.id, text });
|
|
errors.push(text);
|
|
}
|
|
if (res.why) blocked.add(l.to.box);
|
|
return res.why ? null : res.value;
|
|
};
|
|
/** 받는 값 — 쉬는 갈래에서 온 빈 값은 「쉼」 줄만 남김 · 들어오는 선이 모두 쉬는 갈래면 받는 상자도 쉼
|
|
* (하나라도 살아 있으면 셈 · 쉬는 갈래 값은 빈 칸) · 조건 선 바로 뒤 상자는 through 가 막음 */
|
|
const arrive = (box: CfBox): Record<string, CfValue> => {
|
|
const ins = lg.links.filter((l) => l.to.box === box.id);
|
|
const got: Record<string, CfValue> = {};
|
|
let rest = 0;
|
|
for (const l of ins) {
|
|
if (resting.has(l.from.box) && valueOf(l.from.box, l.from.name) === null) {
|
|
rest++;
|
|
got[l.to.name] = null;
|
|
const text = tl("Rest", { from: name(l.from.box), box: name(l.to.box) });
|
|
found.push({
|
|
kind: "통과 안 됨",
|
|
level: "쉼",
|
|
box: l.to.box,
|
|
port: l.to.name,
|
|
link: l.id,
|
|
text,
|
|
});
|
|
continue;
|
|
}
|
|
got[l.to.name] = through(l);
|
|
}
|
|
if (ins.length && rest === ins.length) {
|
|
resting.add(box.id);
|
|
blocked.add(box.id);
|
|
}
|
|
arrived[box.id] = got;
|
|
return got;
|
|
};
|
|
const { boxes, loop } = order(lg);
|
|
for (const b of loop) errors.push(tl("Loop", { box: b.name }));
|
|
const outputs: CfEvalResult["outputs"] = {};
|
|
const cost: CfEvalResult["cost"] = { 노무비: 0, 재료비: 0, 경비: 0, 값들: {}, 계: 0 };
|
|
for (const b of boxes) {
|
|
const given = values[b.id] ?? {};
|
|
if (b.kind === "input" && b.input?.mode !== "logic") {
|
|
ports[b.id] = inputPorts(lg, b, given);
|
|
for (const e of b.input?.elements ?? [])
|
|
if (e.mode === "rule" && ports[b.id][elementPort(e)] === null) {
|
|
const text = tl("NoMaterial", { box: b.name, pill: elementPort(e) });
|
|
found.push({
|
|
kind: "맞는 재료 없음",
|
|
level: "문제",
|
|
box: b.id,
|
|
port: elementPort(e),
|
|
text,
|
|
});
|
|
errors.push(text);
|
|
}
|
|
} else if (b.kind === "input") ports[b.id] = otherLogic(b, arrive(b), given, opts, errors);
|
|
else if (b.kind === "function") {
|
|
const got = arrive(b);
|
|
ports[b.id] = {
|
|
[b.func!.output.name]: blocked.has(b.id) ? null : runFunc(b, got, given, errors),
|
|
};
|
|
} else {
|
|
const v = arrive(b)[OUTPUT_PORT] ?? null;
|
|
let scalar: Scalar = Array.isArray(v) ? null : v;
|
|
const round = b.output?.round;
|
|
if (typeof scalar === "number" && round) scalar = applyRound(round, scalar);
|
|
outputs[b.id] = scalar;
|
|
const kind = b.output?.cost ?? "값";
|
|
if (kind === "값") cost.값들[b.name] = scalar;
|
|
else if (typeof scalar === "number" && Number.isFinite(scalar)) cost[kind] += scalar;
|
|
else if (scalar !== null) errors.push(tl("NotNumber", { box: b.name }));
|
|
}
|
|
}
|
|
cost.계 = cost.노무비 + cost.재료비 + cost.경비;
|
|
gates(lg, passed, name, found, errors);
|
|
return { ports, arrived, outputs, cost, errors, breaks: found };
|
|
}
|
|
|
|
/** 다른 로직 입력 — 그 로직을 받은 값(설계 때 정할 것)으로 셈 · 없으면 넘긴 값 */
|
|
function otherLogic(
|
|
box: CfBox,
|
|
arrive: Record<string, CfValue>,
|
|
given: Record<string, CfValue>,
|
|
opts: CfEvalOptions,
|
|
errors: string[],
|
|
): Record<string, CfValue> {
|
|
const ref = box.input?.logic;
|
|
const out: Record<string, CfValue> = {};
|
|
const other = ref ? opts.logics?.[ref.key] : undefined;
|
|
if (!other || (opts.depth ?? 0) > 8) {
|
|
if (ref && opts.logics && !other) errors.push(tl("NoLogic", { box: box.name }));
|
|
for (const p of ref?.outputs ?? []) out[p.name] = given[p.name] ?? null;
|
|
return out;
|
|
}
|
|
const inner: CfValues = {};
|
|
for (const d of designInputs(other)) {
|
|
// 값 차례 = 이어진 선 > 넘긴 값 > 상자에 둔 예시값 > 그 로직 자기 예시값(inner 에 안 넣음)
|
|
const v = d.port in arrive ? arrive[d.port] : (given[d.port] ?? ref?.examples?.[d.port]);
|
|
if (v !== undefined && v !== null) (inner[d.box] ??= {})[d.port] = v;
|
|
}
|
|
const ev = evalLogic(other, inner, { logics: opts.logics, depth: (opts.depth ?? 0) + 1 });
|
|
for (const e of ev.errors) errors.push(`「${box.name}」 · ${e}`);
|
|
for (const p of ref?.outputs ?? []) {
|
|
const ob = other.boxes.find((b) => b.kind === "output" && b.name === p.name);
|
|
out[p.name] = ob ? (ev.outputs[ob.id] ?? null) : (given[p.name] ?? null);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 한 알약에서 나간 조건 선(둘 이상) — 겹침 · 안 맞음 */
|
|
function gates(
|
|
lg: CfLogic,
|
|
passed: Map<string, boolean>,
|
|
name: (id: string) => string,
|
|
found: CfBreak[],
|
|
errors: string[],
|
|
): void {
|
|
const groups = new Map<string, CfLink[]>();
|
|
for (const l of lg.links)
|
|
if (l.rule?.kind === "gate") {
|
|
const k = `${l.rule.from.box}|${l.rule.from.name}|${l.from.box}|${l.from.name}`;
|
|
groups.set(k, [...(groups.get(k) ?? []), l]);
|
|
}
|
|
for (const list of groups.values()) {
|
|
if (list.length < 2) continue;
|
|
const n = list.filter((l) => passed.get(l.id)).length;
|
|
if (n === 1) continue;
|
|
const g = list[0].rule as Extract<CfLink["rule"], { kind: "gate" }>;
|
|
const fill = { box: name(g.from.box), pill: g.from.name };
|
|
const text = tl(n > 1 ? "Overlap" : "NoneMatch", fill);
|
|
found.push({
|
|
kind: n > 1 ? "조건 겹침" : "조건 안 맞음",
|
|
level: "문제",
|
|
box: g.from.box,
|
|
port: g.from.name,
|
|
text,
|
|
});
|
|
errors.push(text);
|
|
}
|
|
}
|
|
|
|
/* ── 검사 ───────────────────────────────────────────────────────────────── */
|
|
|
|
/** 끊긴 곳 목록 — 빈 받는 자리(다른 로직의 받는 알약은 설계 때 정할 것이라 뺌) + 셈 때 생긴 것 */
|
|
export function breaks(lg: CfLogic, values: CfValues = {}, opts: CfEvalOptions = {}): CfBreak[] {
|
|
const out: CfBreak[] = [];
|
|
for (const b of lg.boxes) {
|
|
if (b.kind === "input") continue;
|
|
for (const p of portsIn(b))
|
|
if (!lg.links.some((l) => l.to.box === b.id && l.to.name === p.name))
|
|
out.push({
|
|
kind: "빈 받는 자리",
|
|
level: "문제",
|
|
box: b.id,
|
|
port: p.name,
|
|
text: tl("EmptyIn", { box: b.name, pill: p.name }),
|
|
});
|
|
}
|
|
return [...out, ...(evalLogic(lg, values, opts).breaks ?? [])];
|
|
}
|
|
|
|
/** 검사 — 빈 받는 자리 · 출력 칸 없음 · 칸에 안 놓인 받는 알약 · 출력 상자 없음 · 값 없는 입력 · 순환 · 오류(쉬운 말 목록) */
|
|
export function checkLogic(lg: CfLogic, values: CfValues = {}, opts: CfEvalOptions = {}): string[] {
|
|
const out: string[] = [];
|
|
for (const b of lg.boxes) {
|
|
if (b.kind !== "input")
|
|
for (const p of portsIn(b))
|
|
if (!lg.links.some((l) => l.to.box === b.id && l.to.name === p.name))
|
|
out.push(tl("EmptyIn", { box: b.name, pill: p.name }));
|
|
if (b.kind === "function" && !parseA1(b.func?.output.cell ?? ""))
|
|
out.push(tl("NoCell", { box: b.name }));
|
|
for (const i of b.func?.inputs ?? [])
|
|
if (!parseA1(i.cell)) out.push(tl("NoInCell", { box: b.name, pill: i.port }));
|
|
}
|
|
if (!lg.boxes.some((b) => b.kind === "output")) out.push(tl("NoOutput"));
|
|
const ev = evalLogic(lg, values, opts);
|
|
for (const b of lg.boxes.filter((x) => x.kind === "input" && x.input?.mode !== "logic"))
|
|
for (const [pill, v] of Object.entries(ev.ports[b.id] ?? {}))
|
|
if (v === null && lg.links.some((l) => l.from.box === b.id && l.from.name === pill))
|
|
out.push(tl("NoValue", { box: b.name, pill }));
|
|
return [...new Set([...out, ...ev.errors])];
|
|
}
|