Skip to content

Commit cd5d3d7

Browse files
flang: восемь расхождений между слоями, печать в C, stdlib и LeetCode
Найдены и закрыты восемь мест, где парсер, проверка типов, интерпретатор и мост совместимости расходились между собой — то есть программа считалась по-разному в зависимости от слоя: 1. порядок над строками принимался рантаймом вместо отказа при проверке типов; 2. сверка примеров шла через Object.is, а функция возвращает список, запись или вариант — структурное равенство обязано быть структурным; 3. псевдоним типа не разворачивался, а псевдоним через самого себя переполнял стек вместо диагностики; 4. «символ N в текст» разбирался с переставленными аргументами; 5. значение варианта в примере теряло имя варианта и принималось на веру; 6. «соединить список по разделителю» было недостижимо из синтаксиса; 7. «пусто X» и «пусто» без аргумента разбирались одинаково, хотя это проверка пустоты и пустой список; 8. вызов функции без аргументов не отличался от свободного имени. На каждый дефект — тест на уровне парсера и сквозной тест через CLI: расхождение между слоями обязано ломать сборку, а не всплывать на чужой модели. Добавлена печать в C с аренным аллокатором (flang/src/emit/c.mjs), стандартная библиотека и решения задач LeetCode как проверяемые примеры языка. Проверено: npm test — 396 пройдено, 4 пропущено (нет тулчейнов dotnet и Go), 712 тестов flang зелёные; .fts → C → gcc/clang с -Werror -pedantic собирается без предупреждений и даёт значения и коды ошибок, совпадающие с ядром на TS.
1 parent 67213ba commit cd5d3d7

85 files changed

Lines changed: 115373 additions & 1080 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,48 @@
1+
import { locate } from "../../../../tools/locate/index.mjs"
2+
13
import { formatCommand } from "./commands.mjs"
24

35
/**
46
* Turn one FTS `Diagnostic` (see src/diagnostics.ts: `{ code, message,
57
* severity, path?, span? }`) into a GitHub workflow-command annotation line.
68
*
7-
* Two different notions of "path" collide here and must be told apart:
9+
* Where the diagnostic belongs is not decided here: `tools/locate` decides it,
10+
* the same module the language server uses, so an annotation on a pull request
11+
* and a squiggle in the editor land on the same character. This function only
12+
* formats what it is given.
13+
*
14+
* `spot` is the result of `locate(...)` — `{ line, column, endLine, endColumn }`
15+
* or `null`. The default resolves what can be resolved without the document
16+
* text (that is: a `span`, and nothing else), which is what a caller that has
17+
* only the diagnostic in hand can honestly offer.
818
*
9-
* - `file` (this function's second argument) is a real filesystem path,
10-
* supplied by the caller, that GitHub can point an annotation at.
11-
* - `diagnostic.path`, when core `fts` produces it, is a JSON-pointer-style
12-
* locator *inside the document* (e.g. "$.utilities[0].examples[1].expected"),
13-
* not a filesystem path — the parser's own diagnostics use `span` for real
14-
* source coordinates, and only the *validator* (which only ever sees an
15-
* already-parsed document, not source text) falls back to a JSON pointer.
16-
* ftsc/ftspec diagnostics, by contrast, put a real module file path in
17-
* `diagnostic.path` — the caller resolves that distinction before calling
18-
* us and passes the result through as `file`.
19+
* Two different notions of "path" collide in `diagnostic.path` and must be told
20+
* apart — a JSON pointer into the document (core `fts`) versus a real file path
21+
* (`ftsc`/`ftspec`). `locate` makes that distinction; see its `classifyPath`.
22+
* The `file` argument here is always a real filesystem path, resolved by the
23+
* caller, that GitHub can point an annotation at.
1924
*
20-
* When there is no `span`, the annotation is pinned to line 1 of `file` (per
21-
* spec) and `diagnostic.path`, if present, is folded into the message text so
22-
* the location information is not silently dropped.
25+
* When nothing located the diagnostic, the annotation is pinned to line 1 of
26+
* `file` (per spec) and `diagnostic.path`, if present, is folded into the
27+
* message text so the location information is not silently dropped.
2328
*/
24-
export function diagnosticToAnnotation(diagnostic, file) {
29+
export function diagnosticToAnnotation(diagnostic, file, spot = locate(diagnostic, null, { fallback: "none" })) {
2530
const severity = diagnostic.severity === "warning" ? "warning" : "error"
2631
const properties = {}
2732
if (file !== undefined) properties.file = file
2833

29-
const span = diagnostic.span
30-
if (span) {
31-
properties.line = span.start.line
32-
properties.col = span.start.column
33-
if (span.end.line !== span.start.line) properties.endLine = span.end.line
34-
if (span.end.line !== span.start.line || span.end.column !== span.start.column) {
35-
properties.endColumn = span.end.column
36-
}
34+
if (spot) {
35+
properties.line = spot.line
36+
properties.col = spot.column
37+
if (spot.endLine !== spot.line) properties.endLine = spot.endLine
38+
if (spot.endLine !== spot.line || spot.endColumn !== spot.column) properties.endColumn = spot.endColumn
3739
} else if (file !== undefined) {
3840
properties.line = 1
3941
properties.col = 1
4042
}
4143
if (diagnostic.code) properties.title = diagnostic.code
4244

43-
const message = span || !diagnostic.path ? diagnostic.message : `${diagnostic.message} (${diagnostic.path})`
45+
const message = spot || !diagnostic.path ? diagnostic.message : `${diagnostic.message} (${diagnostic.path})`
4446

4547
return formatCommand(severity, properties, message)
4648
}

.github/actions/fts-check/lib/ftsRuntime.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ export function checkFtsSource(fts, source) {
8484
diagnostics.push({
8585
code: "FTS_EXAMPLE_MISMATCH",
8686
severity: "error",
87+
/* `testUtilities` reports a non-converging example with no location
88+
at all — only the names of the utility and the example it ran.
89+
Carrying those two names through is what lets `tools/locate` put
90+
the annotation on the `expected` line instead of on line 1. */
91+
utility: result.utility,
92+
example: result.example,
8793
message: result.error
8894
? `utility '${result.utility}', example '${result.example}': ${result.error}`
8995
: `utility '${result.utility}', example '${result.example}': expected ${JSON.stringify(result.expected)}, got ${JSON.stringify(result.actual)}`,

.github/actions/fts-check/lib/run.mjs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { readFile } from "node:fs/promises"
22

3+
import { locate, outline } from "../../../../tools/locate/index.mjs"
4+
35
import { diagnosticToAnnotation } from "./annotate.mjs"
46
import { listAllFtsFiles, listChangedFiles, parsePatterns, resolveBaseRef } from "./discover.mjs"
57
import { findTool, resolveToolDiagnosticFile, runTool } from "./externalTools.mjs"
@@ -54,15 +56,28 @@ export async function run(options) {
5456
for (const file of files) {
5557
const relPath = relative(workspaceDir, file).split("\\").join("/")
5658
const source = await readFile(file, "utf8")
57-
const result = checkFtsSource(fts, source)
59+
/* One outline per file, shared by every diagnostic it produces. It also
60+
carries `compileSource`: the same text with an ftsc module header
61+
blanked out (the core does not compile headers) but with line numbers
62+
untouched, so annotations still point at lines of the real file. */
63+
const view = outline(source)
64+
const result = checkFtsSource(fts, view.compileSource)
5865
models.push({
5966
file: relPath,
6067
hasUtilities: result.hasUtilities,
6168
examplesTotal: result.examplesTotal,
6269
examplesFailed: result.examplesFailed,
6370
diagnostics: result.diagnostics,
6471
})
65-
for (const diagnostic of result.diagnostics) annotations.push(diagnosticToAnnotation(diagnostic, relPath))
72+
/* `fallback: "none"` on purpose. In an editor, guessing a line from a name
73+
quoted in the message is a cheap affordance — the cursor is already
74+
there. On a pull request a guessed annotation lands on a diff line and
75+
misleads, so a diagnostic that carries no location at all keeps the
76+
documented line-1 pin instead. */
77+
for (const diagnostic of result.diagnostics) {
78+
const spot = locate(diagnostic, view, { origin: "core", fallback: "none" })
79+
annotations.push(diagnosticToAnnotation(diagnostic, relPath, spot))
80+
}
6681
}
6782

6883
const tools = []
@@ -79,7 +94,10 @@ export async function run(options) {
7994
tools.push({ name: toolName, diagnostics })
8095
for (const diagnostic of diagnostics) {
8196
const file = resolveToolDiagnosticFile(diagnostic, workspaceDir, workspaceDir)
82-
annotations.push(diagnosticToAnnotation(diagnostic, file))
97+
/* `origin: "tool"` tells `locate` that `diagnostic.path` is a file name
98+
and not a pointer into a document, so it never resolves it against an
99+
outline of some other file. */
100+
annotations.push(diagnosticToAnnotation(diagnostic, file, locate(diagnostic, null, { origin: "tool" })))
83101
}
84102
}
85103

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
FTS is an indentation-based executable specification language. A human-readable `.fts` model can define domain objects, deterministic utilities, executable examples, checked properties, morphisms, and machine-checkable evidence.
44

5-
The repository is intentionally usable at three levels:
5+
The repository is intentionally usable at several levels:
66

7-
- as a TypeScript library (`compile`, `validate`, `executeUtility`, `testUtilities`, `generateTypeScript`, `certify`, `verify`);
7+
- as a library (`compile`, `validate`, `executeUtility`, `testUtilities`, `generateTypeScript`, `certify`, `verify`) — the core is written in TypeScript, but a model is not tied to it: see below;
88
- as a JSON-first CLI (`fts check`, `fts test`, `fts generate`, `fts certify`, `fts verify`);
9-
- as a read-only MCP server exposing the same operations to AI agents.
9+
- as a read-only MCP server exposing the same operations to AI agents;
10+
- as a **project compiler**`ftsc` builds trees of `.fts` modules with checked functors between categories and prints them to **eight languages**: C, Rust, C#, Java, Elixir, Go, Python, TypeScript. A model written once runs natively wherever those compile;
11+
- as a **full language**[`flang`](flang/SPEC.md) adds sum types, collections, strings as data, recursion and pattern matching on top of FTS, while keeping every existing `.fts` a valid program (verified against the core on 19 593 inputs, zero divergences).
12+
13+
TypeScript is how the core is implemented, not what a model is limited to.
1014

1115
```fts
1216
категория «Продажи»
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
модуль «Две суммы»
2+
3+
// LeetCode 1. Two Sum.
4+
// Дан список чисел и цель. Найти два разных элемента, дающих в сумме цель,
5+
// и вернуть их номера. Номера — с нуля, как в условии задачи; язык нумерует
6+
// строки с единицы, поэтому пересчёт делается один раз, в конце.
7+
//
8+
// Тотальная. Хеш-таблиц в языке нет, поэтому вместо словаря «значение → номер»
9+
// работает вложенный проход: для каждой головы ищем дополнение в хвосте.
10+
// Это O(n²) вместо O(n) — прямая цена отсутствия ассоциативного массива.
11+
12+
тотальная функция «Позиция значения»
13+
принимает элементы: список числа, нужное: число
14+
возвращает число
15+
пример «Найдено вторым»
16+
дано элементы равно [5, 7]
17+
дано нужное равно 7
18+
ожидается 2
19+
пример «Не найдено»
20+
дано элементы равно [5, 7]
21+
дано нужное равно 9
22+
ожидается 0
23+
разбор элементов
24+
случай пусто
25+
то 0
26+
случай голова и хвост
27+
если голова равен нужное
28+
то 1
29+
иначе
30+
пусть дальше равно «Позиция значения» от хвост и нужное
31+
если дальше равен 0 то 0 иначе дальше плюс 1
32+
33+
тотальная функция «Поиск пары с позиции»
34+
принимает элементы: список числа, цель: число, позиция: число
35+
возвращает список числа
36+
пример «Пара в начале»
37+
дано элементы равно [2, 7, 11]
38+
дано цель равно 9
39+
дано позиция равно 0
40+
ожидается [0, 1]
41+
разбор элементов
42+
случай пусто
43+
то пустой список
44+
случай голова и хвост
45+
пусть смещение равно «Позиция значения» от хвост и (цель минус голова)
46+
если смещение больше 0
47+
то [позиция, позиция плюс смещение]
48+
иначе «Поиск пары с позиции» от хвост и цель и (позиция плюс 1)
49+
50+
тотальная функция «Две суммы»
51+
принимает элементы: список числа, цель: число
52+
возвращает список числа
53+
пример «Пример 1 из условия»
54+
дано элементы равно [2, 7, 11, 15]
55+
дано цель равно 9
56+
ожидается [0, 1]
57+
пример «Пример 2 из условия»
58+
дано элементы равно [3, 2, 4]
59+
дано цель равно 6
60+
ожидается [1, 2]
61+
пример «Пример 3 из условия»
62+
дано элементы равно [3, 3]
63+
дано цель равно 6
64+
ожидается [0, 1]
65+
«Поиск пары с позиции» от элементы и цель и 0
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
модуль «Римские числа»
2+
3+
// LeetCode 13. Roman to Integer.
4+
// Перевести римскую запись в число.
5+
//
6+
// Обычная, не тотальная — и виновата только строка. Сам разбор («если
7+
// предыдущая цифра меньше текущей, вычесть её дважды») выражается свёрткой
8+
// с записью-состоянием и тотален; но чтобы получить символы строки, её надо
9+
// обойти по одному, а убывание «строка стала короче на символ» анализ
10+
// завершаемости не признаёт: часть значения — это хвост списка, голова,
11+
// поле записи или поле варианта, и ничего больше.
12+
//
13+
// Была бы встроенная форма «символы строки», задача стала бы тотальной
14+
// целиком.
15+
16+
объект «Разбор римского»
17+
сумма является числом
18+
предыдущее является числом
19+
20+
тотальная функция «Приписать строку в начало»
21+
принимает первая: строка, элементы: список строки
22+
возвращает список строки
23+
пример «В непустой»
24+
дано первая равно "I"
25+
дано элементы равно ["V"]
26+
ожидается ["I", "V"]
27+
свёртка элементы начиная с [первая] как акк и эл → добавить эл к акк
28+
29+
тотальная функция «Значение цифры»
30+
принимает буква: строка
31+
возвращает число
32+
пример «Единица»
33+
дано буква равно "I"
34+
ожидается 1
35+
пример «Тысяча»
36+
дано буква равно "M"
37+
ожидается 1000
38+
пример «Не римская цифра»
39+
дано буква равно "щ"
40+
ожидается 0
41+
если буква равен "I"
42+
то 1
43+
иначе
44+
если буква равен "V"
45+
то 5
46+
иначе
47+
если буква равен "X"
48+
то 10
49+
иначе
50+
если буква равен "L"
51+
то 50
52+
иначе
53+
если буква равен "C"
54+
то 100
55+
иначе
56+
если буква равен "D"
57+
то 500
58+
иначе
59+
если буква равен "M" то 1000 иначе 0
60+
61+
функция «Символы с позиции»
62+
принимает текст: строка, позиция: число
63+
возвращает список строки
64+
пример «С последнего символа»
65+
дано текст равно "IV"
66+
дано позиция равно 2
67+
ожидается ["V"]
68+
если позиция больше (длина текст)
69+
то пустой список
70+
иначе
71+
пусть буква равно подстрока текст с позиция по позиция
72+
«Приписать строку в начало» от буква и («Символы с позиции» от текст и (позиция плюс 1))
73+
74+
функция «Римское в число»
75+
принимает текст: строка
76+
возвращает число
77+
пример «Пример 1 из условия»
78+
дано текст равно "III"
79+
ожидается 3
80+
пример «Пример 2 из условия»
81+
дано текст равно "LVIII"
82+
ожидается 58
83+
пример «Пример 3 из условия»
84+
дано текст равно "MCMXCIV"
85+
ожидается 1994
86+
пример «Вычитание»
87+
дано текст равно "IV"
88+
ожидается 4
89+
пусть начальное равно запись «Разбор римского» с сумма равным 0 и предыдущее равным 0
90+
пусть итог равно свёртка («Символы с позиции» от текст и 1) начиная с начальное как акк и буква
91+
пусть значение равно «Значение цифры» от буква
92+
пусть поправка равно если акк.предыдущее меньше значение то (2 умножить на акк.предыдущее) иначе 0
93+
запись «Разбор римского» с сумма равным (акк.сумма плюс значение минус поправка) и предыдущее равным значение
94+
итог.сумма

0 commit comments

Comments
 (0)