Przejdź do treści

Wyrażenia i instrukcje

Rozdział opisuje strukturę wyrażeń oraz zakres obecnej kontroli semantycznej. Poprawne parsowanie i brak diagnostyk nie dowodzą, że operandy mają poprawne typy ani że wygenerowany kod zachowa przenośne znaczenie.

Pierwszeństwo i łączenie operatorów

Od najsilniejszego do najsłabszego wiązania:

Poziom Zapis
Postfiksowy indeks a[i], pole a.b, człon A::B, wywołanie f(...)
Unarny -, not / !, &
Konwersja as Typ
Mnożenie *, /, %
Dodawanie +, -
Relacje <, >, <=, >=
Równość ==, !=
Koniunkcja and / &&
Alternatywa or / ||
Warunkowy warunek ? a : b
Przypisanie =, +=, -=, *=, /=, %=

W01 — struktura drzewa

Nawiasy grupują wyrażenie. Poziomy binarne od mnożenia do alternatywy są budowane przez AstBuilder.BuildLeftAssoc od lewej: a-b-c tworzy (a-b)-c. Nie oznacza to przenośnego kontraktu kolejności wykonywania wszystkich efektów. Powtarzane porównania są składniowo łączone na tym poziomie, nie ustanawiają matematycznej koniunkcji w rodzaju a<b and b<c.

Operator unarny jest rekurencyjny; alternatywa po dwukropku operatora warunkowego również jest rekurencyjna. Reguła przypisania dopuszcza najwyżej jeden operator na tym poziomie. a=b=c zostało odrzucone, zamiast tworzyć domyślny łańcuch przypisań prawostronnych. as występuje opcjonalnie raz na poziomie castExpression.

expression
    : assignmentExpression
    ;
assignmentExpression
    : conditionalExpression (compoundOperator conditionalExpression)?
    ;
compoundOperator
    : ASSIGN
    | PLUS_ASSIGN | MINUS_ASSIGN | MULT_ASSIGN | DIV_ASSIGN | MOD_ASSIGN
    ;
conditionalExpression
    : logicalOrExpression (QUESTION expression COLON conditionalExpression)?
    ;
unaryExpression
    : (MINUS | NOT | AMPERSAND) unaryExpression
    | postfixExpression
    ;
castExpression
    : unaryExpression (AS typeReference)?
    ;
postfixExpression
    : primaryExpression
      ( LBRACKET expression RBRACKET         // index
      | DOT anyIdentifier                    // field access
      | DOUBLE_COLON anyIdentifier           // enum variant access
      | LPAREN argumentList? RPAREN          // function call
      )*
    ;
primaryExpression
    : literal
    | anyIdentifier
    | CONFIG                             // dostęp do bloku config: config.pole
    | LPAREN expression RPAREN
    | structLiteral
    | arrayLiteral
    | jsonLiteral
    | communicationExpression
    ;

W02 — znaczenie nazw i konstrukcji postfiksowych

Gramatyka pozwala składać kolejne indeksy, pola, człony :: i wywołania. Sama postać x(...) nie dowodzi, że x jest wywoływalne, a a.b nie dowodzi istnienia pola. :: obsługuje syntaktycznie człony kwalifikowane, w tym warianty enum i pewne wywołania przez alias modułu; nie daje pełnej kwalifikacji typów.

& jest parsowane jako operator unarny, ale nie ustanawia kompletnego kontraktu pożyczania, własności ani czasu życia referencji. Konwersja as jest konstrukcją składniową; przyjęcie 1 as Float nie dowodzi kompletnej walidacji konwersji.

Wywołania i wartości złożone

argumentList
    : expression (COMMA expression)*
    | namedArgument (COMMA namedArgument)*
    ;
namedArgument
    : IDENTIFIER COLON expression
    ;
structLiteral
    : IDENTIFIER LBRACE structFieldInit (COMMA structFieldInit)* COMMA? RBRACE
    ;
structFieldInit
    : IDENTIFIER COLON expression
    ;
arrayLiteral
    : LBRACKET (expression (COMMA expression)* COMMA?)? RBRACKET
    ;
jsonLiteral
    : LBRACE (jsonField (COMMA jsonField)*)? RBRACE
    ;
jsonField
    : STRING_LITERAL COLON expression
    ;

W03 — argumenty

Lista wywołania jest albo pozycyjna, albo nazwana; gramatyka nie dopuszcza mieszania, np. Add(1,b:2). Nazwa argumentu w tej regule jest IDENTIFIER. Końcowy przecinek nie jest przewidziany przez argumentList.

Rozpoznanie argumentów nazwanych nie stanowi pełnej gwarancji ich wiązania z każdym rodzajem funkcji. Query i mutation storage mają osobne kontrole nazw, liczby i typów. Nie przenosimy tych gwarancji na wszystkie zwykłe funkcje. Próba zwykłego Twice("text") przy parametrze value: Int przeszła check.

W04 — literały złożone

Literał struktury wymaga co najmniej jednego przypisania pola i dopuszcza końcowy przecinek. Literał listy w nawiasach kwadratowych może być pusty i również dopuszcza końcowy przecinek. JSON ma klucze będące tokenami STRING_LITERAL, może być pusty, ale jego reguła nie przewiduje końcowego przecinka. Kontrola reprezentacji i dopuszczalnych typów nie wynika z samych klamer.

Obecne wnioskowanie listy wybiera pierwszy element o ustalonym typie i buduje List<T>. Nie sprawdza w tej funkcji jednorodności całej listy. Próba let value: List<Int> = [1,"text"] przeszła bez diagnostyk; nie uznajemy przez to mieszanej listy za semantycznie poprawną.

Lokalne deklaracje i sterowanie

declarationStatement
    : (LET | CONST | VAR) IDENTIFIER (COLON typeReference)? ASSIGN expression SEMICOLON?
    ;
returnStatement
    : RETURN expression? SEMICOLON?
    ;
ifStatement
    : IF expression LBRACE statement* RBRACE
      (ELSE (ifStatement | LBRACE statement* RBRACE))?
    ;
forStatement
    : FOR anyIdentifier IN expression LBRACE statement* RBRACE
    ;
matchStatement
    : MATCH expression LBRACE matchArm+ RBRACE
    ;
matchArm
    : matchPattern ARROW (LBRACE statement* RBRACE | expression) COMMA?
    ;
matchPattern
    : UNDERSCORE                                     // wildcard
    | literal                                        // literal pattern
    | IDENTIFIER                                     // binding
    | IDENTIFIER DOUBLE_COLON anyIdentifier          // enum variant
    | IDENTIFIER DOUBLE_COLON anyIdentifier
        LPAREN matchPattern (COMMA matchPattern)* RPAREN  // enum variant with data
    ;

W05 — deklaracje

let, const i var wymagają nazwy oraz inicjalizatora; typ jest opcjonalny. Są projektowane odpowiednio jako wiązanie niezmienne, stała i zmienna, ale próba przypisania do const przeszła check bez diagnostyk. Nie przedstawiamy więc niezmienności jako w pełni egzekwowanej własności obecnego analizatora. Sprawdzanie jawnego typu let opisano w typach.

W06 — if, for i match

Gałąź if wymaga bloku w klamrach; else dopuszcza blok albo kolejne if. Intencją jest wybór na podstawie warunku logicznego, lecz if 1 {} przechodzi obecną próbę bez diagnostyk. Brak błędu nie jest regułą niejawnej konwersji do Bool.

for nazwa in wyrażenie ma ciało w klamrach. Iteracja po komunikacji ma odrębne ograniczenia opisane w komunikacji; nie utożsamiamy jej z iteracją po dowolnej kolekcji ani pojedynczym odbiorem wiadomości.

match jest instrukcją i wymaga co najmniej jednej gałęzi. Wzorce obejmują _, literał, nazwę wiązania i wariant enum, również z danymi. Gałąź po -> zawiera wyrażenie albo blok; przecinek jest opcjonalny. Wzorzec x: Int nie należy do tej gramatyki. Sama składnia nie dowodzi kontroli wyczerpujących wzorców, osiągalności gałęzi ani kompletnego zawężania unii.

Wnioskowanie typu nie zastępuje walidacji operatora

InferUnaryType nadaje Bool operatorowi not/!, a pozostałym operatorom unarnym przekazuje typ operandu. InferBinaryType nadaje Bool porównaniom i logice; dla innych operacji wybiera Float przy operandzie rozpoznanym jako Float, a w pozostałych przypadkach dostępny typ lewej albo prawej strony.

Dla operatora warunkowego bierze zgodny typ gałęzi, jeśli go ustalił; w przeciwnym razie może zwrócić pierwszy dostępny typ gałęzi. Nie stanowi to pełnej kontroli zgodności gałęzi ani wyprowadzenia ich unii. InferFieldAccessType może zwrócić brak typu dla nieznanego pola.

Wykonane próby przyjęły bez diagnostyk:

Próba Luka, której brak błędu nie rozstrzyga
not 1 Wymaganie logicznego operandu.
1 + "text" Zgodność operandów operatora dodawania.
if 1 {} Typ warunku.
Przypisanie do const Niezmienność i poprawność celu zapisu.
Odczyt r.missing ze znanej struktury Istnienie pola i pełne typowanie dostępu.
Tekst przekazany do zwykłej funkcji z parametrem Int Zgodność typu argumentu.

Sprawdzone przykłady

Pary są sprawdzane przez rzeczywiste CLI w osobnych projektach. Potwierdzają składnię i wybrane kontrole, nie wynik obliczeń po wykonaniu.

Przykład poprawny

task Decide() @server {
    let x: Int = 2 + 3 * 4
    if x > 10 {
        log("high")
    } else {
        log("low")
    }
}

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Przykład błędny

task Decide() @server { if true log("yes") }

Powód: Instrukcja if wymaga bloku w nawiasach klamrowych.

Kontrola: przykład odrzucony przez check; kody: P0001.

Pełny zakres weryfikacji przykładów.

Przykład: Postacie argumentów wywołania

Poprawny zapis

fn Add(a: Int,b: Int)->Int { return a+b } task Main() @server { let x=Add(a:1,b:2) }

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Błędny zapis

fn Add(a: Int,b: Int)->Int { return a+b } task Main() @server { let x=Add(1,b:2) }

Powód: Lista argumentów dopuszcza postać pozycyjną albo nazwaną; nie ich mieszanie.

Kontrola: przykład odrzucony przez check; kody: P0001.

Pełny zakres weryfikacji przykładów.

Przykład: Blok instrukcji if

Poprawny zapis

task Main() @server { if true { let x = 1 } }

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Błędny zapis

task Main() @server { if true let x = 1 }

Powód: Gałąź if wymaga nawiasów klamrowych.

Kontrola: przykład odrzucony przez check; kody: P0001.

Pełny zakres weryfikacji przykładów.

Przykład: Grupowanie nawiasami

Poprawny zapis

task Main() @server { let x = (1+2)*3 }

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Błędny zapis

task Main() @server { let x = (1+2)* }

Powód: Operator mnożenia wymaga prawego operandu.

Kontrola: przykład odrzucony przez check; kody: P0001.

Pełny zakres weryfikacji przykładów.

Jawne luki

  • Pełne typowanie operandów, warunków, indeksów, pól i wszystkich rodzajów wywołań.
  • Walidacja celu przypisania, niezmienności let/const i operacji złożonych.
  • Jednorodność kolekcji, zgodność gałęzi i zawężanie unii.
  • Wyliczony kontrakt dopuszczalnych konwersji i operatora referencji &.
  • Kolejność efektów w operandach i argumentach oraz pełny kontrakt krótkiego spięcia niezależny od języka hosta.
  • Dzielenie przez zero, przepełnienia, reszta z dzielenia i konwersje numeryczne na wszystkich targetach.
  • Wyczerpywanie i kolejność dopasowań match jako zweryfikowana semantyka wykonania.

Nie wypełniamy tych luk domyślnym zachowaniem Go, C++ lub JavaScript.

Podstawa w repozytorium

  • grammar/JunexisLang.g4: poziomy wyrażeń, argumenty, literały i instrukcje.
  • compiler/JunexisLang.Compiler.Core/Parsing/AstBuilder.cs: budowanie AST, BuildLeftAssoc i normalizacja operatorów logicznych.
  • compiler/JunexisLang.Compiler.Core/Semantics/SemanticAnalyzer.cs: ValidateExpr, ValidateCall, InferExprType, InferUnaryType, InferBinaryType, InferArrayLiteralType i InferFieldAccessType.
  • compiler/JunexisLang.Compiler.Tests/Parser/ParserExpressionTests.cs: testy aliasów operatorów i pierwszeństwa and przed or.
  • compiler/JunexisLang.Compiler.Tests/Semantics/SemanticAnalyzerTests.cs: wybrane przypadki wnioskowania typów i walidacji.