tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

commit 1da4fab351c3c04d2543cea54667f099074e95cb
parent 53f7aa7d422102a47b2ba2415acd86ada2bbde8f
Author: James Eversole <james@eversole.co>
Date:   Mon, 19 Oct 2026 17:28:33 -0500

Fix body continuation relative to its owning layout boundary

Unpublished candidate: plain and annotated definitions share layout; local/arm/do bodies retain their owner across newlines. Delimited inline locals inherit grouping. Suffixes may align but cannot cross an inner dedent. Reject unindented next-line bodies and malformed nested do blocks rather than stealing siblings. Grouped contract expressions permit continuation.

Candidate passes 93 independent value/located-reply cases, 48 local controls, exact reference AST parity for all 95 real consumers, and authenticated source/receipt audits. Python unit models pass. Generation acceptance, fixture/site publication and retention remain pending.

Diffstat:
Mdocs/validation.md | 25+++++++++++++++++++++++++
Mlib/source/parser.tri | 130++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mlib/source/program.tri | 18+++++++-----------
Atest/source_layout.py | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/source_layout_cases.py | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/unit.py | 15+++++++++++++++
6 files changed, 412 insertions(+), 72 deletions(-)

diff --git a/docs/validation.md b/docs/validation.md @@ -1246,6 +1246,31 @@ UTF-8 boundary checks; classifier tests alone are not consumer or generation acceptance. Performance measurements must use matched complete workloads, not only individual predicates. +## Source layout controls (opt-in) + +```sh +nix develop -c python3 test/source_layout.py result/bin/tricu \ + --directory "$FRESH_CHECK" +``` + +Rebuilds the expression/program parsers and every affected consumer through +`source_unit`, using the authenticated packaged compiler and source-verified +unchanged fixture dependencies. Audits full native manifests and dependency +receipts. Independent complete values and located parser replies cover owner- +relative continuation, annotated heads, grouping, lists, local bindings, case +arms, do statements, pipes, comments and missing-body boundaries. Existing +local-control diagnostics run against the rebuilt unit too. + +The harness compares complete parser replies for every `.tri` file under +`lib/`, `demos/` and `tricu-apps/` against the reference parser, replacing native +header lines with equal-length comments to preserve body positions. This is +body-grammar compatibility coverage, not native import resolution or application +execution. A reference-parser negative control must fail the corrected layout +oracle. `--resume` explicitly reuses diagnostic compilation checkpoints; it does +not constitute fresh-generation acceptance. Each child is bounded, with an +8 GiB sampled RSS guard and a thirty-minute runner budget. Compiler publication +still requires the generation, smoke, fixture and retention gates. + ## Compiler name-index controls (opt-in) ```sh diff --git a/lib/source/parser.tri b/lib/source/parser.tri @@ -63,38 +63,30 @@ sourceGateParseBindingWith valueParser input = sourceEquationBinding valueParser sourceGateParseBinding expression = sourceGateParseBindingWith expression --- Shared layout transition after `=`, `in`, or another binding boundary. --- Inline values use the enclosing expression context. After newlines, the --- next located token selects the new indentation context without rewriting --- source text or exposing recursion driven by a known indentation counter. -sourceGateParseLayoutFirst expression indentedExpression label input = - lazyList - (_ : parserReplyFail (parserDiagnostic 40 label t t t) - input false false) - (token rest : - lazyBool - (_ : expression input) - (_ : indentedExpression - (sourcePositionColumn (sourceTokenStart token)) input) - (equal? (sourcePositionColumn (sourceTokenStart token)) 1)) - input +-- A body belongs to its definition/statement even when its first operand +-- follows a newline. Never reset the continuation boundary to that operand. +-- Validate before consuming it so a sibling cannot become a missing body. +sourceGateParseLayoutFirst expression mode label input = (lazyList + (_ : parserReplyFail (parserDiagnostic 40 label t t t) input false false) + (token rest : lazyBool + (_ : expression input) + (_ : parserReplyFail (parserDiagnostic 40 label (sourceTokenKind token) + (sourceTokenStart token) t) input false false) + (or? (fst mode) + (not? (lte_? (sourcePositionColumn (sourceTokenStart token)) (snd mode))))) input) -sourceGateParseLayoutValue expression indentedExpression label input = - lazyList +sourceGateParseLayoutValue expression mode label input = (lazyList + (_ : expression input) + (token rest : lazyBool + (_ : parserThen sourceGateSkipNestedNewlines + (sourceGateParseLayoutFirst expression mode label) input) (_ : expression input) - (token rest : - lazyBool - (_ : parserThen sourceGateSkipNestedNewlines - (sourceGateParseLayoutFirst expression indentedExpression label) - input) - (_ : expression input) - (equal? (sourceTokenKind token) sourceTokenNewline)) - input + (equal? (sourceTokenKind token) sourceTokenNewline)) input) -- Local groups share the program head grammar, but are deliberately not -- dependency-sorted: nested nonrecursive lets expose only earlier groups. sourceGateLocalValue indentedExpression column input = - sourceGateParseLayoutValue (indentedExpression column) indentedExpression + sourceGateParseLayoutValue (indentedExpression column) (sourceGateLayoutIndented column) "local binding value" input sourceGateLocalNext? input column = (lazyList (_ : false) (token rest : lazyBool @@ -116,11 +108,10 @@ sourceGateLocalGroupsReply bindings input = (lazyResult (diagnostic remainder : parserReplyFail diagnostic input false false) (_ unused : parserReplyOk groups input false false) (sourceEquationUnique groups)) (sourceEquationGroups bindings)) -sourceGateLocalBindings indentedExpression input = (lazyList +sourceGateLocalBindings valueParser indentedExpression input = (lazyList (_ : sourceGateAtomError input) (token rest : parserBind - (sourceEquationBinding (sourceGateLocalValue indentedExpression - (sourcePositionColumn (sourceTokenStart token)))) + (sourceEquationBinding valueParser) (first : parserBind (parserMany (sourceGateLocalNext indentedExpression (sourcePositionColumn (sourceTokenStart token)))) (remaining : sourceGateLocalGroupsReply (pair first remaining))) input) input) @@ -130,17 +121,23 @@ sourceGateLocalBuild bindings body start end = (foldr sourceGateLocalIndented indentedExpression parentColumn input = (lazyList (_ : parserReplyFail (parserDiagnostic 40 "indented local block" t t t) input false false) (token rest : lazyBool - (_ : sourceGateLocalBindings indentedExpression input) + (_ : sourceGateLocalBindings + (sourceGateLocalValue indentedExpression (sourcePositionColumn (sourceTokenStart token))) + indentedExpression input) (_ : parserReplyFail (parserDiagnostic 40 "indented local block" (sourceTokenKind token) (sourceTokenStart token) t) input false false) (and? (not? (lte_? (sourcePositionColumn (sourceTokenStart token)) parentColumn)) (not? (lte_? (sourcePositionColumn (sourceTokenStart token)) 1)))) input) -sourceGateLocalStart indentedExpression parentColumn input = (lazyList - (_ : sourceGateLocalBindings indentedExpression input) +-- An inline binding inherits its enclosing expression context (including +-- explicit grouping). A newline after let/where opens a separate local block. +sourceGateLocalStart expression indentedExpression mode input = (lazyList + (_ : sourceGateLocalBindings + (sourceGateParseLayoutValue expression mode "local binding value") indentedExpression input) (token rest : lazyBool (_ : parserThen sourceGateSkipNestedNewlines - (sourceGateLocalIndented indentedExpression parentColumn) input) - (_ : sourceGateLocalBindings indentedExpression input) + (sourceGateLocalIndented indentedExpression (snd mode)) input) + (_ : sourceGateLocalBindings + (sourceGateParseLayoutValue expression mode "local binding value") indentedExpression input) (equal? (sourceTokenKind token) sourceTokenNewline)) input) -- rec applies to exactly one contiguous function group. It is not a mutually -- recursive block and never changes the scope of plain let/where bindings. @@ -154,17 +151,17 @@ sourceGateLocalFinish _ _ _ start _ input = sourceGateRecursive = (parserMap (present : lazyMaybe (_ : false) (_ : true) present) (parserOptional (sourceGateExpect sourceTokenRec))) -sourceGateParseLet expression indentedExpression parentColumn input = (parserBind +sourceGateParseLet expression indentedExpression mode input = (parserBind (sourceGateExpect sourceTokenLet) (keyword : parserBind sourceGateRecursive (recursive : - parserBind (sourceGateLocalStart indentedExpression parentColumn) (bindings : + parserBind (sourceGateLocalStart expression indentedExpression mode) (bindings : parserThen sourceGateSkipNestedNewlines (parserThen (sourceGateExpect sourceTokenIn) - (parserBind (sourceGateParseLayoutValue expression indentedExpression "local binding body") + (parserBind (sourceGateParseLayoutValue expression mode "local binding body") (body : sourceGateLocalFinish recursive bindings body (sourceTokenStart keyword) (sourceAstEnd body))))))) input) -sourceGateParseWhere expression indentedExpression body parentColumn input = (parserThen +sourceGateParseWhere expression indentedExpression body mode input = (parserThen (sourceGateExpect sourceTokenWhere) (parserBind sourceGateRecursive (recursive : - parserBind (sourceGateLocalStart indentedExpression parentColumn) (bindings : + parserBind (sourceGateLocalStart expression indentedExpression mode) (bindings : sourceGateLocalFinish recursive bindings body (sourceAstStart body) (sourceAstEnd (snd (foldl (_ binding : binding) (fst bindings) bindings)))))) input) @@ -174,7 +171,7 @@ sourceGateCaseArm expression column input = (parserBind sourceEquationParse (pat parserThen (sourceGateExpect sourceTokenAssign) (parserMap (body : pair [pattern] body) (sourceGateParseLayoutValue (tokens : expression tokens (sourceGateLayoutIndented column)) - (indent tokens : expression tokens (sourceGateLayoutIndented indent)) "case arm body"))) input) + (sourceGateLayoutIndented column) "case arm body"))) input) sourceGateCasePatternStarts? kind = (or? (sourceGateAtomStarts? kind) (equal? kind sourceTokenPin)) sourceGateCaseSameColumn? input column = (lazyList (_ : false) @@ -268,7 +265,7 @@ sourceGateParseDoStatement expression indentedExpression column input = (parserMap (action : pair (just name) action) (sourceGateParseLayoutValue (tokens : expression tokens (sourceGateLayoutIndented column)) - indentedExpression "do binding action"))) input) + (sourceGateLayoutIndented column) "do binding action"))) input) (_ : parserMap (action : pair nothing action) (tokens : expression tokens (sourceGateLayoutIndented column)) input) @@ -283,7 +280,7 @@ sourceGateParseDoNext expression indentedExpression column input = (sourceGateExpected "do statement" t t) input false false) (sourceGateDoSameColumn? input column) -sourceGateParseDoFirst expression indentedExpression keyword operator input = +sourceGateParseDoFirst expression indentedExpression parentColumn keyword operator input = lazyList (_ : parserReplyFail (parserDiagnostic 40 "indented do block" t t t) input false false) @@ -311,16 +308,17 @@ sourceGateParseDoFirst expression indentedExpression keyword operator input = (parserDiagnostic 40 "indented do block" (sourceTokenKind token) (sourceTokenStart token) t) input false false) - (not? (lte_? (sourcePositionColumn (sourceTokenStart token)) 1))) + (and? (not? (lte_? (sourcePositionColumn (sourceTokenStart token)) parentColumn)) + (not? (lte_? (sourcePositionColumn (sourceTokenStart token)) 1)))) input -sourceGateParseDo expression indentedExpression atom input = +sourceGateParseDo expression indentedExpression parentColumn atom input = parserBind (sourceGateExpect sourceTokenDo) (keyword : parserThen sourceGateSkipNestedNewlines (parserBind atom (operator : parserThen (sourceGateExpect sourceTokenNewline) (parserThen sourceGateSkipNestedNewlines - (sourceGateParseDoFirst expression indentedExpression + (sourceGateParseDoFirst expression indentedExpression parentColumn keyword operator))))) input -- Dispatch only after observing the first token. A selected parser consumes @@ -331,9 +329,9 @@ sourceGateParseAtom expression mode input = (lazyList (token _ : (let indented column tokens = expression tokens (sourceGateLayoutIndented column) in let parse = (case sourceTokenKind token of ^sourceTokenCase = sourceGateParseCase expression mode - ^sourceTokenDo = (sourceGateParseDo expression indented + ^sourceTokenDo = (sourceGateParseDo expression indented (snd mode) (sourceGateParseAtom expression sourceGateLayoutTop)) - ^sourceTokenLet = sourceGateParseLet (tokens : expression tokens mode) indented (snd mode) + ^sourceTokenLet = sourceGateParseLet (tokens : expression tokens mode) indented mode ^sourceTokenT = parserMap sourceAstLeaf (sourceGateExpect sourceTokenT) ^sourceTokenName = parserMap sourceAstName (sourceGateExpect sourceTokenName) ^sourceTokenNumber = parserMap sourceAstNumber (sourceGateExpect sourceTokenNumber) @@ -346,7 +344,7 @@ sourceGateParseAtom expression mode input = (lazyList -- Only a successful atom consumes another operand. A close paren, colon or -- newline is left intact for the enclosing parser; an opened but malformed -- group propagates its consumed failure instead of silently ending the app. --- A newline is a continuation only inside a delimiter and only when a +-- A newline continues inside a delimiter or deeper than its owner, when a -- following token can start an atom. Looking ahead prevents a newline before -- ')' or 'where' from becoming a committed atom failure. sourceGateAtomStarts? kind = @@ -365,25 +363,30 @@ sourceGateLayoutTop = pair false 0 sourceGateLayoutNested = pair true 0 sourceGateLayoutIndented column = pair false column -sourceGateNewlineStarts? tokens predicate mode = +sourceGateNewlineStartsWith? tokens predicate mode inclusive = lazyList (_ : false) (token rest : lazyBool - (_ : sourceGateNewlineStarts? rest predicate mode) + (_ : sourceGateNewlineStartsWith? rest predicate mode inclusive) (_ : and? (predicate (sourceTokenKind token)) (or? (fst mode) - (and? (not? (isZero_? (snd mode))) - (not? (lte_? (sourcePositionColumn (sourceTokenStart token)) - (snd mode)))))) + (lazyBool + (_ : lte_? (snd mode) (sourcePositionColumn (sourceTokenStart token))) + (_ : and? (not? (isZero_? (snd mode))) + (not? (lte_? (sourcePositionColumn (sourceTokenStart token)) (snd mode)))) + inclusive))) (equal? (sourceTokenKind token) sourceTokenNewline)) tokens +sourceGateNewlineStarts? tokens predicate mode = + sourceGateNewlineStartsWith? tokens predicate mode false + -- Do not consume a newline until a matching token at an allowed indentation -- is visible. This retains layout boundaries in parser remainders. -sourceGateAfterLayoutNewlines mode starts parser input = +sourceGateAfterNewlines mode inclusive starts parser input = lazyList (_ : parser input) (token rest : @@ -391,11 +394,16 @@ sourceGateAfterLayoutNewlines mode starts parser input = (_ : lazyBool (_ : parserThen sourceGateSkipNestedNewlines parser input) (_ : parser input) - (sourceGateNewlineStarts? rest starts mode)) + (sourceGateNewlineStartsWith? rest starts mode inclusive)) (_ : parser input) (equal? (sourceTokenKind token) sourceTokenNewline)) input +-- Operators and `where` may align with their owner, but cannot cross a +-- dedent out of an arm, local binding or do statement. +sourceGateAfterLayoutNewlines mode = sourceGateAfterNewlines mode false +sourceGateAfterSuffixNewlines mode = sourceGateAfterNewlines mode true + sourceGateAppNext mode expression input = sourceGateAfterLayoutNewlines mode sourceGateAtomStarts? (sourceGateParseAtom expression mode) input @@ -421,11 +429,11 @@ sourceGateParsePipeOperator = sourceGateParsePipeSegment mode expression input = parserBind - (sourceGateAfterLayoutNewlines sourceGateLayoutNested + (sourceGateAfterSuffixNewlines mode sourceGatePipeStarts? sourceGateParsePipeOperator) - (operator : parserThen sourceGateSkipNestedNewlines - (parserMap (right : pair operator right) - (sourceGateParseApplication mode expression))) input + (operator : parserMap (right : pair operator right) + (sourceGateParseLayoutValue (sourceGateParseApplication mode expression) + mode "pipe operand")) input -- Application binds tighter than a left-associative pipe chain. Retain an -- explicit located AST pipe; core resolution chooses the direction later. @@ -443,12 +451,12 @@ sourceGateExprWithLayout input mode = (sourceGateParsePipes mode sourceGateExprWithLayout) (body : parserMap (suffix : lazyMaybe (_ : body) (local : local) suffix) - (parserOptional (sourceGateAfterLayoutNewlines sourceGateLayoutNested + (parserOptional (sourceGateAfterSuffixNewlines mode (kind : equal? kind sourceTokenWhere) (sourceGateParseWhere (tokens : sourceGateExprWithLayout tokens mode) (column tokens : sourceGateExprWithLayout tokens (sourceGateLayoutIndented column)) - body (snd mode))))) input) + body mode)))) input) (sourceGateLambdaHead? input) sourceGateExpr = (input : sourceGateExprWithLayout input sourceGateLayoutTop) diff --git a/lib/source/program.tri b/lib/source/program.tri @@ -20,12 +20,10 @@ sourceDefinitionBody definition = snd definition sourceProgramSkipNewlines = parserMap (_ : t) (parserMany (sourceGateExpect sourceTokenNewline)) --- Both definition and local-binding bodies use the same located transition: --- unindented lines are top-level; indented values may continue on lines whose --- first token starts in a strictly deeper column. -sourceProgramDefinitionValue = - sourceGateParseLayoutValue sourceGateExpr sourceGateExprIndented - "definition body" +-- Plain and annotated heads share the owning definition's layout boundary. +sourceProgramDefinitionValue column = + sourceGateParseLayoutValue (sourceGateExprIndented column) + (sourceGateLayoutIndented column) "definition body" -- Look ahead across the head (including delimited pattern newlines). This -- distinguishes `f t` (expression) from `f x = x` (definition), without @@ -47,7 +45,7 @@ sourceProgramDefinitionHead? tokens = -- Keep the runtime Result checks (including short-circuit and value passing) -- identical to Frontend.ContractDesugar's supported base-library forms. sourceProgramContract = - parserOr (sourceGateParseGroup sourceGateExpr) + parserOr (sourceGateParseGroup (tokens : sourceGateExprWithLayout tokens sourceGateLayoutNested)) (parserMap sourceAstName (sourceGateExpect sourceTokenName)) -- Argument = pair (Maybe namedBinderToken) (Maybe contractExpression). @@ -214,7 +212,7 @@ sourceProgramParseAnnotatedBinding input = sourceProgramContract)) (result : parserMap (body : pair name (sourceProgramLowerDefinition name args result body)) - sourceProgramDefinitionValue)) remaining) + (sourceProgramDefinitionValue (sourcePositionColumn (sourceTokenStart name))))) remaining) (_ : parserReplyFail (parserDiagnostic 47 "supported contract head" t (sourceTokenStart name) t) remaining true false) @@ -234,9 +232,7 @@ sourceProgramAnnotated? input = (lazyList (_ : false) sourceProgramParsePlainBinding input = (lazyList (_ : sourceGateAtomError input) (token rest : sourceEquationBinding - (sourceGateParseLayoutValue - (sourceGateExprIndented (sourcePositionColumn (sourceTokenStart token))) - sourceGateExprIndented "definition body") input) input) + (sourceProgramDefinitionValue (sourcePositionColumn (sourceTokenStart token))) input) input) sourceProgramParseDefinition input = lazyBool (_ : lazyBool (_ : sourceProgramParseAnnotatedBinding input) diff --git a/test/source_layout.py b/test/source_layout.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Rebuild owner-relative layout and its consumers; check independent trees. + +Opt-in, through Nix, using the authenticated packaged stable compiler. Unchanged +transitive fixture sources are verified before reuse. Never publishes fixtures. +""" +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import time + +from bundle_dag import merge_bundles, read +from fixture_sources import snapshot_bytes, verify +from pattern_lowering import audit +from shipped_compiler import verify as verify_compiler +from source_order import Runner, field +from http_headers import LEAF, TRUE, expect +from local_control import decode_text +from source_layout_cases import semantic_cases, parser_cases +from tooling_batch_models import chars + +ROOT = Path(__file__).resolve().parent.parent + + +def sha(data): + return hashlib.sha256(data).hexdigest() + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('tricu', type=Path) + p.add_argument('--directory', type=Path, required=True) + p.add_argument('--resume', action='store_true', help='diagnostic reuse only, not generation acceptance') + args = p.parse_args() + root = args.directory.resolve() + root.mkdir(parents=True, exist_ok=args.resume) + workspace = root / 'workspace' + workspace.mkdir(exist_ok=args.resume) + (workspace / 'tricu.workspace').write_text('tricu.workspace.v1\n') + runner = Runner(args.tricu, root) + metadata = verify_compiler() + fixture_rows = {row[2]: row for row in verify(ROOT / 'test/fixtures')} + entries = {} + for line in (ROOT / 'bootstrap/compiler-source-map.v1').read_text().splitlines()[1:]: + _, alias, _, path, deps = line.split('\t') + entries[alias] = (path, [] if deps == '-' else deps.split(',')) + needed = [] + + def visit(alias): + if alias not in needed: + for dep in entries[alias][1]: + visit(dep) + needed.append(alias) + visit('source_unit') + affected = {'source_parser', 'source_program'} + for alias in needed: + if any(dep in affected for dep in entries[alias][1]): + affected.add(alias) + active = affected | {dep for alias in affected for dep in entries[alias][1]} + report = dict(complete=False, resumed=args.resume, compiler_sha256=metadata['sha256'], + runtime_sha256=sha(runner.exe.read_bytes()), sources={}, reused={}, cases=[], + harness_sha256=sha(Path(__file__).read_bytes()), + cases_sha256=sha((ROOT / 'test/source_layout_cases.py').read_bytes())) + try: + for alias in needed: + data = (ROOT / entries[alias][0]).read_bytes() + report['sources'][alias] = sha(data) + if alias not in affected: + assert snapshot_bytes(ROOT / 'test/fixtures', fixture_rows[alias][4]) == data, ('stale reuse', alias) + tool = runner.imported(ROOT / metadata['bundle'], 'producer') + manifest = (runner.store / 'objects' / tool[:3] / tool).read_text() + producer = next(line.split('\t')[3] for line in manifest.splitlines()[1:] + if line.split('\t')[1] == 'sourceBuildGraph') + for alias in needed: + if alias not in active: + continue + path = workspace / (alias + '.tri') + if alias in affected: + path.write_bytes((ROOT / entries[alias][0]).read_bytes()) + else: + cohort, _, _, _, source_pin = fixture_rows[alias] + fixture = ROOT / 'test/fixtures' / (cohort + '.arboricx') + bundle = root / (alias + '-retained.arboricx') + merge_bundles(bundle, [(fixture, [(name, name[len(alias)+1:]) for name in read(fixture)[3] + if name.startswith(alias + '.')])], prune_unreachable=True) + pin = runner.imported(bundle, alias + '-retained') + path.write_text('!export * from ' + pin + '\n') + report['reused'][alias] = dict(manifest=pin, source_pin=source_pin) + for alias in ['source_unit', 'source_program', 'source_parser']: + runner.run('compile-' + alias, ['compile', '--file', workspace / (alias + '.tri'), + '--store', runner.store, '--toolchain', tool, '--tool-store', runner.store, + '--output', root / (alias + '.arboricx'), '--compile-timeout', '600'], seconds=630) + report['modules'] = audit(workspace, runner.store, producer, report['reused']) + pins = {alias: runner.imported(root / (alias + '.arboricx'), alias) + for alias in ['source_unit', 'source_program', 'source_parser']} + for alias, pin in pins.items(): + assert pin == report['modules'][alias]['manifest'] + env = root / 'environment.tree' + env.write_bytes(LEAF) + for label, source, value in semantic_cases(): + path = root / (label + '.source.tree') + path.write_bytes(chars(source)) + answer = runner.invoke(label, pins['source_unit'], 'compileStableModuleDefinitions', + ['--arg-tree-file', env, '--arg-tree-file', path]) + expect(field(answer, 0), TRUE, label + ' compilation') + bindings, found = field(answer, 1, 0), None + while bindings != LEAF: + binding = field(bindings, 0) + if decode_text(field(binding, 0)) == 'main': + found = field(binding, 1) + bindings = field(bindings, 1) + expect(found, value, label + ' independent value') + expect(field(answer, 1, 1), LEAF, label + ' remainder') + report['cases'].append(dict(label=label, sha256=sha(answer))) + for label, source, operation, expected in parser_cases(): + path = root / (label + '.source.tree') + path.write_bytes(chars(source)) + alias = 'source_program' if operation == 'parseGateProgram' else 'source_parser' + answer = runner.invoke(label, pins[alias], operation, ['--arg-tree-file', path]) + expect(answer, expected, label + ' complete located reply') + report['cases'].append(dict(label=label, sha256=sha(answer))) + # Parse every real compiler/library/demo consumer with both parsers. + # Replace only native header lines with equal-length line comments: + # body positions remain exact, and no imports need execution here. + reference = root / 'reference-program.arboricx' + cohort = fixture_rows['source_program'][0] + fixture = ROOT / 'test/fixtures' / (cohort + '.arboricx') + merge_bundles(reference, [(fixture, [('source_program.parseGateProgram', 'parseGateProgram')])], + prune_unreachable=True) + reference_pin = runner.imported(reference, 'reference-program') + report['consumers'] = [] + for source_path in sorted({p for directory in ['lib', 'demos', 'tricu-apps'] + for p in (ROOT / directory).rglob('*.tri')}): + text = source_path.read_text() + body = ''.join(('--' + ' ' * (len(line.rstrip('\n')) - 2) + ('\n' if line.endswith('\n') else '')) + if line.startswith('!') else line for line in text.splitlines(keepends=True)) + path = root / 'consumer.source.tree' + path.write_bytes(chars(body)) + label = 'consumer-' + str(source_path.relative_to(ROOT)).replace('/', '-') + before = runner.invoke(label + '-reference', reference_pin, 'parseGateProgram', ['--arg-tree-file', path]) + after = runner.invoke(label, pins['source_program'], 'parseGateProgram', ['--arg-tree-file', path]) + expect(field(before, 0), TRUE, label + ' reference parse') + expect(after, before, label + ' existing source AST and spans') + report['consumers'].append(dict(path=str(source_path.relative_to(ROOT)), source_sha256=sha(text.encode()), + reply_sha256=sha(after))) + # A frozen parser must fail the new independent next-line alignment + # oracle. This prevents accidentally testing the reference again. + label, source, operation, expected = next(row for row in parser_cases() if row[0] == 'ast-layout-2') + path = root / 'old-layout-control.source.tree' + path.write_bytes(chars(source)) + before = runner.invoke('old-layout-control', reference_pin, operation, ['--arg-tree-file', path]) + assert before != expected, 'retired first-body-token layout passed the new oracle' + report['old_layout_rejected'] = True + # Existing local matching/recursion and diagnostic controls run against + # these genuinely rebuilt consumers, not a frozen parser fixture. + control = root / ('local-controls-' + str(time.time_ns())) + subprocess.run(['python3', str(ROOT / 'test/local_control.py'), str(runner.exe), + '--unit', str(root / 'source_unit.arboricx'), + '--program', str(root / 'source_program.arboricx'), + '--directory', str(control)], check=True, timeout=300) + report['local_controls'] = str(control) + for alias in needed: + assert sha((ROOT / entries[alias][0]).read_bytes()) == report['sources'][alias], 'source changed during probe' + report['complete'] = True + except Exception as error: + report['failure'] = str(error) + raise + finally: + report['seconds'] = time.monotonic() - runner.started + (root / 'report.json').write_text(json.dumps(report, indent=2) + '\n') + print(f'PASS: {len(report["cases"])} layout cases, local controls and source/receipt audits') + + +if __name__ == '__main__': + main() diff --git a/test/source_layout_cases.py b/test/source_layout_cases.py @@ -0,0 +1,119 @@ +"""Owner-relative layout oracles: authored values and complete located replies. + +No tricu parser generates expectations. Contract adapters deliberately pass a +value through; these tests exercise annotated-head layout, not contract policy. +""" +from http_headers import LEAF, number, pair, tree_list +from source_grammar_models import Located, ast_apply, diagnostic, chars, reply, authored + + +IDENTITY = 'id x = x\n' +CONTRACTS = ('withContract contract value = contract value\n' + 'matchResult failure success value = success value t\n' + 'err message rest = t\nidentity x = x\n') + + +def semantic_cases(): + rows = [] + def add(label, source, expected=number(23)): + rows.append((label, source, expected)) + for label, body in [ + ('inline', 'id 23'), + ('inline-continuation', 'id\n 23'), + ('next-line-aligned', '\n id\n 23'), + ('next-line-deeper', '\n id\n 23'), + ('next-line-variable-indent', '\n id\n 23'), + ('comments', '\n id -- operand next\n\n -- comment\n 23'), + ('block-comment', '\n id |- ignored\n comment -|\n 23'), + ('grouped', '(id\n23)'), + ]: + add(label, IDENTITY + 'main = ' + body) + add('return-' + label, CONTRACTS + IDENTITY + 'main =@identity ' + body) + add('argument-' + label, CONTRACTS + IDENTITY + 'f x@identity = ' + body + '\nmain = f 99') + add('grouped-contract', CONTRACTS + IDENTITY + 'main =@(id\n identity)\n id\n 23') + add('application-not-nesting', 'first a b = a\nmain =\n first\n 23\n 24') + add('following-definition', 'main =\n id\n 23\nid x = x') + add('equations', 'pick t =\n id\n 23\npick _ = 24\nid x = x\nmain = pick t') + add('local-block', IDENTITY + 'main = let\n value =\n id\n 23\n other = 24\nin value') + add('local-inline', IDENTITY + 'main = let value =\n id\n 23\nin value') + add('local-body', IDENTITY + 'main = let value = 23 in\n id\n value') + add('where-block', IDENTITY + 'main = value where\n value =\n id\n 23\n other = 24') + add('where-inline', IDENTITY + 'main = value where value =\n id\n 23') + add('where-top-dedent', 'main = case t of\n t = value\n _ = 24\nwhere value = 23') + add('where-arm-aligned', 'main = case t of\n t = value\n where value = 23\n _ = 24') + add('pipe-top-dedent', 'main = case t of\n t = 24\n _ = 25\n|> (x : 23)') + add('pipe-arm-aligned', 'main = case t of\n t = 24\n |> (x : 23)\n _ = 25') + add('pipe-next-line', IDENTITY + 'main =\n 23\n |>\n id') + add('pipe-application', 'first a b = a\nmain =\n first\n 23\n <| 24') + add('case-body', IDENTITY + 'main = case t of\n t =\n id\n 23\n _ = 24') + add('case-nested', IDENTITY + 'main = case t of\n t = case t of\n t =\n id\n 23\n _ = 24\n _ = 25') + add('do-binding', IDENTITY + 'pass value next = next value\nmain = do pass\n x <-\n id\n 23\n x') + add('do-expression', IDENTITY + 'pass value next = next value\nmain = do pass\n id\n 99\n id\n 23') + add('do-nested', IDENTITY + 'pass value next = next value\nmain = do pass\n x <- do pass\n y <-\n id\n 23\n y\n x') + add('list-atoms', 'main = [\n 23\n 24\n]', tree_list([number(23), number(24)])) + add('list-group', IDENTITY + 'main = [(id\n 23)\n 24]', tree_list([number(23), number(24)])) + add('grouped-inline-local', IDENTITY + 'main = (let value =\nid\n23 in value)') + add('grouped-inline-where', IDENTITY + 'main = (value where value =\nid\n23)') + add('lambda-body', 'main = (x :\n x\n) 23') + add('local-shadow-boundary', 'value = 24\nmain = let\n value =\n id\n 23\n id x = x\nin value\nid x = x') + return rows + + +def parser_cases(): + rows = [] + # Full program ASTs distinguish application association, token spans and + # untouched sibling definitions; evaluations alone could hide those bugs. + for index, source in enumerate([ + 'main = t t', 'main = t\n t', 'main =\n t\n t', + 'main =\n t\n t', 'main =\n t\n t', + 'main =\n t -- comment\n\n t', + 'main =\n t\n t\nother = 24', + 'main =\n t\n t\n t', + ]): + x = Located(source) + second = x.node(1, LEAF, source.rindex('t'), source.rindex('t')+1) if '--' in source else x.atom(1, 't', 2) + body = ast_apply(x.atom(1, 't'), second) + if source.endswith('\n t\n t\n t'): + body = ast_apply(body, x.atom(1, 't', 3)) + definitions = [pair(x.atom(2, 'main'), body)] + if 'other' in source: + definitions.append(pair(x.atom(2, 'other'), x.atom(6, '24'))) + rows.append(('ast-layout-' + str(index), source, 'parseGateProgram', + reply(True, pair(tree_list(definitions), LEAF), consumed=True))) + # Existing single-expression and delimiter protocols remain intentional. + x = Located('t\nx') + suffix = tree_list([x.token(6, LEAF, 1, 2), x.atom(2, 'x')]) + rows.append(('ast-top-separation', x.text, 'parseGateSource', + reply(False, diagnostic(2, LEAF, number(6), x.pos(1)), suffix, True))) + x = Located('(t\n t)') + rows.append(('ast-group', x.text, 'parseGateSource', reply(True, + x.node(5, ast_apply(x.atom(1, 't'), x.atom(1, 't', 2)), 0, len(x.text)), consumed=True))) + x = Located('[t\n t]') + rows.append(('ast-list', x.text, 'parseGateSource', reply(True, + x.node(8, tree_list([x.atom(1, 't'), x.atom(1, 't', 2)]), 0, len(x.text)), consumed=True))) + # Missing bodies cannot steal a following sibling token. Check complete + # diagnostics AND exact unconsumed located tokens / commitment flags. + for label, source, expected, kind in [ + ('missing-body', 'main =\n', 'definition body', None), + ('unindented-body', 'main =\nt', 'definition body', 1), + ('missing-arm-body', 'main = case t of\n _ =\n t', 'case arm body', 1), + ('nested-do-indent', 'main = do bind\n do bind\n t', 'indented do block', 1), + ('missing-do-action', 'main = do bind\n x <-\n t', 'do binding action', 1), + ('missing-local-value', 'main = let\n x =\n t', 'local binding value', 1), + ('missing-pipe-operand', 'main = t |>\nt', 'pipe operand', 1), + ]: + x = Located(source) + # The last authored token is the unconsumed sibling. + at = None if kind is None else source.rindex('t') + suffix = LEAF if at is None else tree_list([x.token(kind, chars('t'), at, at+1)]) + rows.append((label, source, 'parseGateProgram', reply(False, + diagnostic(40, chars(expected), LEAF if kind is None else number(kind), + LEAF if at is None else x.pos(at)), suffix, True))) + # Keep the pre-existing independent parser protocol controls, including + # consumed failures and malformed delimiters, against the rebuilt parser. + for label, operation, argument, expected in authored(): + if operation in ('expr', 'program'): + from local_control import decode_text + rows.append(('grammar-' + label, decode_text(argument), + 'parseGateProgram' if operation == 'program' else 'parseGateSource', expected)) + return rows diff --git a/test/unit.py b/test/unit.py @@ -1380,6 +1380,21 @@ class GrammarOracleTests(unittest.TestCase): for tree in row: self.assertEqual(end(tree), len(tree)) + def test_layout_oracles_are_complete_and_independent(self): + from source_layout_cases import semantic_cases, parser_cases + from source_order import end + values, replies = semantic_cases(), parser_cases() + self.assertEqual(len(values + replies), len({row[0] for row in values + replies})) + for label, _, value in values: + self.assertEqual(end(value), len(value), label) + for label, _, _, expected in replies: + self.assertEqual(end(expected), len(expected), label) + aligned = next(row[3] for row in replies if row[0] == 'ast-layout-2') + deeper = next(row[3] for row in replies if row[0] == 'ast-layout-4') + self.assertNotEqual(aligned, deeper) # equal structure, different spans + self.assertTrue(any(row[0] == 'unindented-body' for row in replies)) + self.assertTrue(any(row[0] == 'nested-do-indent' for row in replies)) + def test_lookahead_boundaries_do_not_consume_or_guess_assignments(self): from source_grammar_models import lookahead self.assertTrue(lookahead([2, 2, 5], [1, 3, 5])[0][0])