Basic Syntax
Sec source is deliberately regular: declarations use explicit keywords, blocks use braces, ordinary statements do not use semicolons, and punctuation is assigned narrow, consistent meanings.
This chapter introduces the surface syntax used throughout the rest of the manual. Detailed semantics belong to the chapters for types, functions, control flow, ownership, error handling and other language features.
Source Text
Sec source files are UTF-8 text.
- Invalid UTF-8 is rejected.
- An initial UTF-8 byte-order mark is accepted and ignored.
- A byte-order mark anywhere else in the file is invalid.
- LF, CRLF and CR are recognized as line endings.
- The formatter writes LF by default.
Outside literals and comments, ordinary lexical whitespace consists of space, tab and line-ending characters. Other Unicode spacing characters are rejected rather than silently treated as whitespace.
let value := 10
let other := 20
These declarations have the same meaning. Formatting is canonicalized by the formatter.
No statement semicolons
Ordinary statements are not terminated with semicolons:
let value := 10
value += 1
return value
This is not canonical Sec:
let value := 10;
The compiler diagnoses a semicolon used as an ordinary statement terminator.
Source Files
Ordinary Sec source files use the .sec extension.
A canonical source file has this high-level order:
#target(os: "linux", arch: "amd64") // optional
module server
import (
"fmt"
"net/http"
)
type Server struct {
port: uint16,
}
fn main() void {
}
The optional #target directive, when present, comes first.
The module declaration follows, then imports, then top-level declarations.
Ordinary executable statements are not permitted directly at module scope.
Modules
Every ordinary Sec source file declares exactly one source module:
module orders
The source-level module declaration contains one identifier only.
Dotted module declarations are invalid:
module domain.orders
module internal.storage
Directory hierarchy belongs to the canonical import path, not to the source module name.
domain/orders/
create.sec
order.sec
validation.sec
Each file in that directory normally declares:
module orders
All applicable .sec files in one source directory contribute to
one shared module declaration namespace. Source-file order has no semantic
meaning.
Imports
Imports use canonical logical paths written with / separators:
import "fmt"
import "domain/orders"
import sys "platform/linux"
Imports may also be grouped:
import (
"fmt"
"domain/orders"
sys "platform/linux"
)
The imported module's declared module name becomes the default qualifier. The import path itself does not become a dotted source name.
import "customer/order"
let value := order.CreateCustomerOrder()
An alias changes only the local qualifier:
import customerOrder "customer/order"
let value := customerOrder.CreateCustomerOrder()
Import bindings are local to the source file that declares them.
Imports do not inject declarations as unqualified names:
import "storage"
let file := storage.Open()
The following is not made valid merely by the import:
let file := Open()
Selective imports, wildcard imports and implicit re-exports are not part of Sec 0.1.
Identifiers
An identifier begins with a Unicode letter or underscore.
Following characters may be Unicode letters, ASCII digits or underscores.
value
currentValue
HTTPServer
_speed
__state
Ω
Σ
μs
Meter2
Identifiers are:
- case-sensitive;
- required to already be in Unicode NFC form;
- not locale-sensitive;
- not escapable when they collide with a reserved word.
Value
value
These are different identifiers.
Underscore has special forms
The bare spelling _ is contextual and is not an ordinary binding name.
_
_name
__name
_name and __name are ordinary identifiers.
Their prefixes participate in Sec visibility rules:
| Form | General visibility meaning |
|---|---|
Name | Public |
_name | Module-internal |
__name | Source-file-private |
The bare _ is used only where another grammar rule assigns it a
meaning, such as discard positions, wildcard patterns or reserved register bits.
Comments
Line comments
// Calculate the next value.
let next := value + 1
A line comment begins with // and continues to the end of the line.
Block comments
/*
* This is a block comment.
*/
Block comments may span multiple lines and may nest:
/*
* Outer comment.
*
* /*
* * Inner comment.
* */
*
* Outer comment continues.
*/
Documentation comments
A block comment beginning with /** is a documentation comment:
/**
* Returns the current temperature.
*/
fn CurrentTemperature() Temperature {
return temperature
}
Documentation comments are lexically comments, but tooling may attach them to declarations for generated documentation, hover information and editor support.
/// is an ordinary line comment.
Literals
Boolean literals
true
false
Sec has no ordinary nil keyword.
Integer literals
0
42
1_000_000
0b1111_0000
0o755
0xFFFF_FFFF
Binary, octal and hexadecimal forms use explicit prefixes. Leading zeroes on decimal literals do not imply octal.
Fractional and exponent literals
3.14
.5
1.25e-3
1E6
A decimal point uses ., never a locale-specific comma.
1. is not a fractional literal; write 1.0.
Unsuffixed fractional and exponent forms follow Sec's exact-decimal inference. A numeric family suffix may request another family.
8i
8u
8g
8m
65t
65r
1.5g
1.5m
| Suffix | Numeric family |
|---|---|
i | Signed integer |
u | Unsigned integer |
g | Binary floating point |
m | Exact decimal |
t | char scalar |
r | rune scalar |
The suffix selects a family, not a fixed bit width.
Character literals
'A'
'\n'
'Ω'
After escape processing, a character literal contains exactly one Unicode scalar value.
Strings
Ordinary strings use double quotes:
"hello"
"hello\nworld"
Raw strings use backticks and do not process backslash escapes:
let text := `first line
second line`
Interpolated strings begin with $":
let message := $"Hello {name}"
Literal braces in an interpolated string are doubled:
let text := $"{{value}}"
This produces the text {value}.
Declarations
Sec uses explicit declaration forms rather than deriving declaration kind from context.
Common declaration forms include:
| Syntax | Purpose |
|---|---|
module | Declares the source module. |
import | Creates a source-file-local module binding. |
type | Declares a named type. |
enum | Declares an enum. |
unit | Declares a semantic unit. |
interface | Declares an interface contract. |
fn | Declares a function or method. |
impl | Declares behavior and associated members for a type. |
let | Declares a binding. |
static | Modifies a declaration to give it static storage or type-associated semantics where permitted. |
extern | Declares an external function boundary. |
test | Declares a test in an applicable *_test.sec file. |
Named types
type Percent int range 0..100
type Speed decimal<m/s>
Structs
type Point struct {
x: int,
y: int,
}
Functions
fn Add(left: int, right: int) int {
return left + right
}
Impl blocks
impl Point {
fn IsOrigin() bool {
return self.x == 0 && self.y == 0
}
}
Ordinary impl methods receive compiler-provided implicit self.
Only static methods omit instance binding entirely.
Variables
let declarations are immutable by default:
let count := 10
let name: string := "Sec"
Mutability is explicit:
let mut count := 10
count = 11
Multiple declarators may share one let statement:
let width := 10, height := 20
Type-first declarations are also part of the language:
int mut: x, y, z
float: width := 5.4, pi := 3.14
Immutable declarations require initialization. Mutable typed declarations may omit an initializer only when the declared type is defaultable.
Explicit move initialization
Move intent has dedicated declaration syntax:
let buffer :<- source
let buffer: Buffer <- source
The first form infers the type. The second names the destination type explicitly.
Copy/direct construction uses :=.
Assignment
Assignment is statement syntax, not an expression that produces a value.
count = 10
buffer <- replacement
= is ordinary assignment. <- is explicit move assignment.
Which transfers are copies, moves or invalid is determined by ownership semantics.
Compound assignment is supported:
count += 1
flags |= mask
value <<= 2
A compound assignment evaluates the destination once and the source once. It is not defined as naïve textual rewriting.
Blocks and Statements
Braces define blocks:
{
let value := 10
Use(value)
}
Sec does not use indentation as block syntax.
Control-flow blocks require braces:
if ready {
Start()
} else {
Wait()
}
Normal conditions do not require parentheses:
while connected {
Poll()
}
The condition must still have type bool.
A simple infinite loop uses:
for {
Tick()
}
Iteration uses in:
for value in values {
Use(value)
}
C-style semicolon-separated loop headers are not part of Sec.
Expressions and Operators
Common operator families include:
// arithmetic
+ - * / %
// comparison
< <= > >= == !=
// logical
! && ||
// bitwise
~ & | ^ << >>
// membership
in
Logical && and || short-circuit.
Ordered comparison and equality operators do not form comparison chains.
Precedence
From higher to lower precedence:
| Level | Forms |
|---|---|
| Member | . |
| Postfix | calls, indexing/slicing, struct literals, spread |
| Prefix | +, -, !, ~ |
| Multiplicative | *, /, %, contextual x |
| Additive | +, - |
| Shift | <<, >> |
| Comparison | <, <=, >, >=, in |
| Equality | ==, != |
| Bitwise | &, then ^, then | |
| Logical | &&, then || |
Assignment is not part of expression precedence because assignment is a statement.
let value := a + b * c
is interpreted as:
let value := a + (b * c)
Parentheses may override precedence or document intent.
Ranges
Sec has two range operators:
.. // inclusive upper bound
..< // exclusive upper bound
Examples:
1..10
0..<10
..100
0..
Range syntax is contextual in Sec 0.1. It is accepted where the surrounding grammar defines a range, such as iteration, membership, contracts, slicing or switch cases.
if value in 0..<10 {
Use(value)
}
A standalone range is not automatically a first-class Range value.
Punctuation
Several punctuation families carry stable language-wide meaning:
| Syntax | Meaning family |
|---|---|
() | Parameters, arguments, grouping and selected construction forms |
{} | Blocks, declaration bodies and literals |
[] | Generic parameters/arguments, arrays, sequences, indexing and slicing |
<> | Unit expressions in numeric type syntax |
: | Typed identifiers and grammar-specific separators |
:= | Declaration initialization |
= | Ordinary assignment |
<- | Explicit move assignment or transfer where defined |
:<- | Inferred declaration with explicit move initialization |
=> | Match/handler arm mapping |
@ | Attributes and special annotations |
# | Compiler directives |
... | Expansion/spread family |
.. | Inclusive range |
..< | Exclusive-upper range |
The lexer uses longest-token matching, so punctuation such as
:<-, ..<, <<= and
=> is recognized as the longest valid token at that position.
Formatting
Sec has a canonical formatter. Formatting choices should not become a second language layered on top of the grammar.
The formatter is expected to:
- preserve comments;
- preserve literal values;
- preserve raw-string content;
- write canonical spaces around operators;
- remove trailing whitespace;
- write one final newline;
- avoid semicolon insertion;
- preserve valid identifier spelling after NFC validation;
- keep declaration metadata attached to the declaration it describes.
Canonical formatting may remove syntax that is provably redundant, but it must not change program meaning.