aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-26 10:49:02 +0100
committereunhhu <[email protected]>2026-09-26 10:49:02 +0100
commitb55395802f4c7b929c45013efb572aab786a8b43 (patch)
treead57b9d3521521c6ce9ca04d6a7f151b05c824b2
parent2d7024ba01875a8fc7a2c99e78c62911bcc7fe3f (diff)
downloadvega-b55395802f4c7b929c45013efb572aab786a8b43.tar.gz
vega-b55395802f4c7b929c45013efb572aab786a8b43.zip
Add vega check for conventions a spec misses
Surfaces read a view that misses a convention as something else without a word: a number label makes a list a table, a text tone disappears, a list never shows its other columns, X_focus without a table becomes a field, a second text input has nowhere to type. vega check MODULE names each such place (the view or input and what the surface does instead) and exits 1; the rules sit next to the code that reads the screen. The checks build a spec that misses eight conventions and expect exactly its nine findings, and none in the examples.
-rw-r--r--docs/design/001-first-component.md26
-rw-r--r--src/main.c21
-rw-r--r--src/screen.c56
-rw-r--r--src/vega.h5
-rw-r--r--tests/check.py59
5 files changed, 163 insertions, 4 deletions
diff --git a/docs/design/001-first-component.md b/docs/design/001-first-component.md
index db04bad..5033389 100644
--- a/docs/design/001-first-component.md
+++ b/docs/design/001-first-component.md
@@ -46,6 +46,29 @@ Tab이 키 모드로 바꾸면 글자·숫자·Space가 `KeyA`·`Digit0`·`Space
표시 폭은 Vega가 맡는다. 한글·CJK·그림 이모지는 두 칸, 결합 문자·변이 선택자·ZWJ는 0칸, 제어 문자는
제어 그림(U+2400대)으로 보인다. grapheme 단위 자르기는 아직 없어 ZWJ 이모지 묶음은 근사다.
+## 규약 검사 (`vega check`)
+
+`vega check app.pmod`는 모듈이 뜻한 듯하지만 규약에 맞지 않는 곳을 알린다. 표면은 이런 보기를 조용히 다른
+부품으로 읽는다. 검사는 그 읽기를 드러내어 오타나 타입 실수가 화면에서 사라지지 않게 한다. 찾으면 종료 코드 1이다.
+규칙은 화면을 읽는 코드와 같은 파일(`src/screen.c`)에 있다.
+
+| 찾는 것 | 표면이 대신 하는 일 |
+|---|---|
+| 수인 `title` | 필드로 보인다 |
+| 수인 `label` 열 | 목록이 아니라 표로 보인다 |
+| text인 `tone` 열 | 목록은 무시, 표는 열로 보인다 |
+| 수인 `badge`·`detail` 열 | 보이지 않는다 |
+| 목록의 그 밖의 열 | 보이지 않는다(목록은 label, badge, detail, tone만 보인다) |
+| 보일 열이 없는 표 | 제목과 행 수만 보인다 |
+| 목록·표가 없는 `X_focus`, text인 `_focus` | 필드로 보이거나 포커스가 없다 |
+| 두 번째 text 입력 | 입력할 곳이 없다(첫 번째만 입력 줄을 얻는다) |
+
+```text
+$ vega check bad.pmod
+bad.pmod: items_list.tone: a tone is a number (vega.normal ... vega.danger); the list ignores it
+bad.pmod: missing_focus: names no list or table missing; it shows as a field
+```
+
## 영속
모든 표면은 `--store 경로`를 받는다. 앱의 상태는 Pure가 그 트리 저장소에 두므로, 터미널에서 적은 할 일을 다음 실행의
@@ -64,10 +87,11 @@ CLI나 문장 표면이 그대로 이어받는다. Vega에도 앱에도 저장 �
- 한 저장소를 CLI와 문장 표면이 번갈아 이어 쓰며(시드마다 7번) 매번 모델과 같은 목록을 보이고, 대화형 세션에서 입력한
것이 다음 실행에 남는다.
- aup와 Vega의 예제 11개를 세 표면에 무작위 입력으로 돌려 비정상 종료가 없다. sanitizer 빌드로도 같은 검사를 통과한다.
+- 규약을 여덟 가지로 어긴 명세에서 `vega check`가 아홉 곳을 모두 정확한 문장으로 알리고, 예제에서는 아무것도 알리지
+ 않는다.
## 남은 것
-- 규약 오류를 조용히 넘기지 않기: `label`이 수인 목록, 표에 없는 `_focus` 같은 것을 `vega check`로 알린다.
- 포커스를 표면에서 바꾸기(GUI 클릭, TUI 마우스). 지금은 앱의 키 핸들러만 포커스를 옮긴다. Pure에 표의 인덱스를
이벤트로 바꾸는 호스트 API가 필요하다.
- 셀 단위 갱신(TUI는 매 변경마다 전체를 다시 그린다), GUI 표면, 음성 합성으로 가는 NUI.
diff --git a/src/main.c b/src/main.c
index 1d8cc4b..3fcf105 100644
--- a/src/main.c
+++ b/src/main.c
@@ -11,6 +11,7 @@
#include <unistd.h>
/* vega SURFACE MODULE [--store PATH]: one module, one screen, several surfaces.
+ * check what the module seems to mean but does not meet (exit 1)
* tui interactive terminal
* tui --dump WxH the terminal's frames as text, driven by a script
* cli plain lines for pipes, driven by a script
@@ -29,7 +30,7 @@ static char entry[VEGA_TEXT];
static size_t entry_length;
static bool typing;
-static bool load(const char *path, const char *store) {
+static bool load_module(const char *path) {
static unsigned char bytes[1 << 17];
FILE *file = fopen(path, "rb");
size_t size = file ? fread(bytes, 1, sizeof(bytes), file) : 0;
@@ -41,6 +42,15 @@ static bool load(const char *path, const char *store) {
fprintf(stderr, "vega: %s: %s\n", path, error);
return false;
}
+ return true;
+}
+
+static void print_finding(void *path, const char *name, const char *message) {
+ printf("%s: %s: %s\n", (const char *)path, name, message);
+}
+
+static bool load(const char *path, const char *store) {
+ if (!load_module(path)) return false;
if (store && pure_store_init(store) != PURE_OK) { fprintf(stderr, "vega: cannot open store %s\n", store); return false; }
PureResult started = pure_app_start(&app, &module, store ? pure_tree_data(store) : (PureData){0});
if (started != PURE_OK) { fprintf(stderr, "vega: cannot start (%d)\n", (int)started); return false; }
@@ -376,11 +386,16 @@ int main(int argc, char **argv) {
width <= 1000 && height <= 1000) dumped = true;
else usage = true;
}
- if (usage || (dumped && strcmp(argv[1], "tui"))) {
+ if (usage || (dumped && strcmp(argv[1], "tui")) || (!strcmp(argv[1], "check") && argc != 3)) {
fputs("vega tui|cli|say MODULE.pmod [--store PATH]\n"
- "vega tui MODULE.pmod [--store PATH] --dump WIDTHxHEIGHT < script\n", stderr);
+ "vega tui MODULE.pmod [--store PATH] --dump WIDTHxHEIGHT < script\n"
+ "vega check MODULE.pmod\n", stderr);
return 2;
}
+ if (!strcmp(argv[1], "check")) {
+ if (!load_module(argv[2])) return 2;
+ return vega_check(&module, print_finding, argv[2]) ? 1 : 0;
+ }
Surface surface = !strcmp(argv[1], "cli") ? SURFACE_CLI : !strcmp(argv[1], "say") ? SURFACE_SAY :
!strcmp(argv[1], "tui") ? SURFACE_TUI : (Surface)-1;
if ((int)surface < 0) { fprintf(stderr, "vega: unknown surface %s\n", argv[1]); return 2; }
diff --git a/src/screen.c b/src/screen.c
index 8e584a5..2d4c6dc 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -50,6 +50,62 @@ void vega_screen(VegaScreen *screen, const PureModule *m) {
}
}
+static bool is_view(const PureModule *m, int view, uint8_t type) {
+ return view >= 0 && m->view_type[view] == type;
+}
+
+unsigned vega_check(const PureModule *m, void (*report)(void *, const char *name, const char *message), void *context) {
+ unsigned found = 0;
+ char name[2 * PURE_MODULE_NAME + 8], message[256];
+#define FIND(...) do { snprintf(message, sizeof(message), __VA_ARGS__); report(context, name, message); ++found; } while (0)
+ int title = pure_module_view(m, "title");
+ snprintf(name, sizeof(name), "title");
+ if (title >= 0 && m->view_type[title] != PURE_VIEW_TEXT) FIND("a title is text; this one shows as a field");
+ for (uint32_t i = 0; i < m->views; ++i) {
+ if (m->view_table[i]) continue;
+ const char *view = m->view_names[i];
+ size_t length = strlen(view);
+ snprintf(name, sizeof(name), "%s", view);
+ if (length > 6 && !strcmp(view + length - 6, "_focus")) {
+ char table[PURE_MODULE_NAME];
+ snprintf(table, sizeof(table), "%.*s", (int)(length - 6), view);
+ int owner = pure_module_view(m, table);
+ if (!is_view(m, owner, PURE_VIEW_TABLE))
+ FIND("names no list or table %s; it shows as a field", table);
+ else if (m->view_type[i] != PURE_VIEW_NUM) FIND("the focused row is a number; %s shows no focus", table);
+ }
+ if (m->view_type[i] != PURE_VIEW_TABLE) continue;
+ int label = pure_module_column(m, i, "label"), tone = pure_module_column(m, i, "tone");
+ bool list = is_view(m, label, PURE_VIEW_TEXT);
+ unsigned shown = 0;
+ for (uint32_t k = 0; k < m->views; ++k) {
+ if (m->view_table[k] != i + 1) continue;
+ const char *column = m->view_names[k];
+ bool text = m->view_type[k] == PURE_VIEW_TEXT;
+ snprintf(name, sizeof(name), "%s.%s", view, column);
+ if ((int)k == label && !text) FIND("a list's label is text; %s shows as a table", view);
+ else if ((int)k == tone && text) FIND("a tone is a number (vega.normal ... vega.danger); %s", list ?
+ "the list ignores it" : "it shows as a column");
+ else if (list && (!strcmp(column, "badge") || !strcmp(column, "detail")) && !text)
+ FIND("a list's %s is text; this one is not shown", column);
+ else if (list && (int)k != label && (int)k != tone && strcmp(column, "badge") && strcmp(column, "detail"))
+ FIND("a list shows label, badge, detail and tone; this column is not shown");
+ shown += (int)k != tone || text;
+ }
+ snprintf(name, sizeof(name), "%s", view);
+ if (!list && !shown) FIND("a table with no column to show");
+ }
+ int prompt = -1;
+ for (uint32_t i = 0; i < m->inputs; ++i) {
+ if (m->input[i].kind != PURE_KIND_TEXT) continue;
+ snprintf(name, sizeof(name), "%s", m->input_names[i]);
+ if (prompt >= 0) FIND("only the first text input (%s) has a place to type into", m->input_names[prompt]);
+ else prompt = (int)i;
+ }
+#undef FIND
+ return found;
+}
+
typedef struct { char *out; size_t used; } Collect;
static bool collect(void *context, PureSpan span) {
diff --git a/src/vega.h b/src/vega.h
index 40aecaa..062639b 100644
--- a/src/vega.h
+++ b/src/vega.h
@@ -34,6 +34,11 @@ typedef struct {
} VegaScreen;
void vega_screen(VegaScreen *, const PureModule *);
+/* What a module seems to mean but does not meet: a list label that is not
+ * text, a _focus view without its table, a column a list never shows, a
+ * second text input. Each finding names the view or input it is about;
+ * returns how many there were. */
+unsigned vega_check(const PureModule *, void (*report)(void *, const char *name, const char *message), void *context);
/* A view (row ignored) or a column at a row as text; numbers in Pure's
* shortest form. false when it has no value (for example a missing item). */
diff --git a/tests/check.py b/tests/check.py
index 6256023..8b86c71 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -16,6 +16,8 @@
4. Every aup example runs on every surface under random keys without a crash.
5. Stored: surfaces take turns on one store and each continues the list; an
interactive session's entries are there in the next one.
+6. vega check names every convention a spec seems to mean but misses, and
+ none in the examples.
"""
import argparse
import json
@@ -304,6 +306,61 @@ def interactive(vega, module, store=None):
return 1
+BROKEN_SPEC = """use vega
+record Item {
+ name: text
+ size = 0
+}
+state items: list of Item
+state at: index of items
+input add = text
+input note = text
+input up = key ArrowUp
+on add { items.append(Item { name: add }) }
+on note { items.append(Item { name: note }) }
+on up.down { at = at - 1 }
+view title = 3
+view items_list = each item in items {
+ label = item.name
+ tone = "red"
+ badge = item.size
+ size = item.size
+}
+view sizes = each item in items {
+ label = item.size
+}
+view empty = each item in items {
+ tone = vega.muted
+}
+view missing_focus = 1
+view items_list_focus = "0"
+"""
+
+
+def conventions(vega, aup, work, modules):
+ """Every convention the broken spec misses, and none in the examples."""
+ env = dict(os.environ, AUP_PATH=str(ROOT / 'lib'))
+ (work / 'broken.aup').write_text(BROKEN_SPEC)
+ built = run(aup, 'build', work / 'broken.aup', '-o', work / 'broken.pmod', env=env)
+ assert built.returncode == 0, built.stderr
+ result = run(vega, 'check', work / 'broken.pmod')
+ found = [line.split(': ', 1)[1] for line in result.stdout.splitlines()]
+ assert result.returncode == 1 and found == [
+ 'title: a title is text; this one shows as a field',
+ 'items_list.tone: a tone is a number (vega.normal ... vega.danger); the list ignores it',
+ "items_list.badge: a list's badge is text; this one is not shown",
+ 'items_list.size: a list shows label, badge, detail and tone; this column is not shown',
+ "sizes.label: a list's label is text; sizes shows as a table",
+ 'empty: a table with no column to show',
+ 'missing_focus: names no list or table missing; it shows as a field',
+ 'items_list_focus: the focused row is a number; items_list shows no focus',
+ 'note: only the first text input (add) has a place to type into'], result.stdout
+ for module in modules.values():
+ clean = run(vega, 'check', module)
+ assert clean.returncode == 0 and not clean.stdout, clean.stdout
+ return len(found)
+
+
def robustness(vega, aup, work, rounds):
"""Every aup example, every surface, random keys and text: never a crash."""
examples = sorted((ROOT.parent / 'aup' / 'examples').glob('*.aup')) + sorted((ROOT / 'examples').glob('*.aup'))
@@ -315,6 +372,7 @@ def robustness(vega, aup, work, rounds):
module = work / (path.stem + '.pmod')
built = run(aup, 'build', path, '-o', module, env=env)
assert built.returncode == 0, (path, built.stderr)
+ assert run(vega, 'check', module).returncode in (0, 1), path
rng = random.Random(path.stem)
for _ in range(rounds):
script = []
@@ -357,6 +415,7 @@ def main():
report['interactive_sessions'] = interactive(vega, modules['todo']) + \
interactive(vega, modules['todo'], work / 'interactive-store')
report['stored_surface_lines'] = stored_surfaces(vega, modules['todo'], work, range(args.seeds))
+ report['convention_findings'] = conventions(vega, aup, work, modules)
report['examples'], report['robustness_runs'] = robustness(vega, aup, work, args.rounds)
print(json.dumps(report))