Documentation
¶
Overview ¶
Package semantic provides semantic analysis for CalcMark programs.
The semantic checker validates CalcMark Abstract Syntax Trees (ASTs) for semantic correctness without executing them. It catches errors like undefined variables, type mismatches, and incompatible units.
Architecture ¶
The semantic checker operates in three phases:
- Environment Setup: Tracks variable definitions and their types
- AST Traversal: Visits each node and validates semantics
- Diagnostic Collection: Accumulates errors, warnings, and hints
Usage ¶
Basic validation:
checker := semantic.NewChecker()
diagnostics := checker.Check(astNodes)
for _, diag := range diagnostics {
if diag.Severity == semantic.Error {
fmt.Printf("Error: %s\n", diag.Message)
}
}
With pre-populated environment:
checker := semantic.NewChecker()
checker.GetEnvironment().Set("x", types.NewNumber(decimal.NewFromInt(5)))
diagnostics := checker.Check(astNodes)
Diagnostic Codes ¶
The checker produces structured diagnostics with specific codes:
- DiagUndefinedVariable: Variable used before definition
- DiagIncompatibleUnits: Incompatible units in operation (e.g., "5 kg + 10 meters")
- DiagInvalidCurrency: Unknown currency code
- DiagTypeMismatch: Type error in operation
- DiagDivisionByZero: Division or modulus by zero
Severity Levels ¶
- Error: Prevents evaluation, must be fixed
- Warning: Valid syntax but may cause runtime issues
- Hint: Style suggestions for improvement
Unit Validation ¶
The semantic checker validates unit compatibility:
- Compatible: "5 kg + 10 lb" (both mass)
- Incompatible: "5 kg + 10 meters" (mass + length)
- Currency: Validates against known ISO 4217 codes
See unit_validation.go for detailed unit compatibility rules.
Performance ¶
Semantic checking is fast and typically completes in microseconds. It's designed to run on every keystroke in interactive editors.
Index ¶
- Constants
- Variables
- func AreUnitsCompatible(unit1, unit2 string) bool
- func GetExamplesForType(argType ArgType) []string
- func IsDataSizeUnit(unit string) bool
- func NormalizeCurrencySymbol(symbolOrCode string) (string, bool)
- func ValidateCurrencyCode(code string) bool
- type ArgType
- type Checker
- type DataSizeBase
- type Diagnostic
- func CheckTypeCompatibility(left, right TypeInfo, operator string, r *ast.Range) *Diagnostic
- func CreateIncompatibleCurrenciesDiagnostic(code1, code2 string, operation string, node ast.Node) *Diagnostic
- func CreateInvalidCurrencyDiagnostic(code string, node ast.Node) *Diagnostic
- func ValidateCurrencyCodeWithDiagnostic(code string) (valid bool, diag *Diagnostic)
- type Environment
- func (e *Environment) Clone() *Environment
- func (e *Environment) Get(name string) (types.Type, bool)
- func (e *Environment) GetAllVariables() map[string]types.Type
- func (e *Environment) GetInfo(name string) (*VarInfo, bool)
- func (e *Environment) Has(name string) bool
- func (e *Environment) Set(name string, value types.Type)
- func (e *Environment) SetWithRange(name string, value types.Type, r *ast.Range)
- type FrontmatterInfo
- type FunctionSpec
- type ParamSpec
- type QuantityType
- type Severity
- type TypeInfo
- type TypeKind
- type VarInfo
Constants ¶
const ( // Currency diagnostics DiagInvalidCurrencyCode = "invalid_currency_code" DiagIncompatibleCurrencies = "incompatible_currencies" // Type diagnostics DiagTypeMismatch = "type_mismatch" DiagInvalidDateOperation = "invalid_date_operation" DiagUnsupportedUnit = "unsupported_unit" DiagIncompatibleUnits = "incompatible_units" // Date diagnostics (USER REQUIREMENT) DiagInvalidDate = "invalid_date" DiagInvalidMonth = "invalid_month" DiagInvalidDay = "invalid_day" DiagInvalidYear = "invalid_year" DiagInvalidLeapYear = "invalid_leap_year" // Variable diagnostics DiagUndefinedVariable = "undefined_variable" DiagVariableRedefinition = "variable_redefinition" // Arithmetic diagnostics DiagDivisionByZero = "division_by_zero" // Data size unit hints DiagMixedBaseUnits = "mixed_base_units" // Directive diagnostics DiagInvalidDirective = "invalid_directive" DiagUndefinedGlobal = "undefined_global" DiagMissingFrontmatter = "missing_frontmatter" )
DiagnosticCode constants for all diagnostic types
Variables ¶
var ArgTypeExamples = map[ArgType][]string{ ArgTypeNumber: {"42", "3.14", "1000", "1e6"}, ArgTypeQuantity: {"10 meters", "5 kg", "100 MB", "2.5 liters"}, ArgTypeRate: {"10 MB/s", "60 km/h", "100 requests/second", "5 GB/day"}, ArgTypeDuration: {"2 hours", "30 minutes", "1 day", "500 ms"}, ArgTypePercentage: {"99.9%", "15%", "50%", "0.1%"}, ArgTypeString: {`"datacenter"`, `"ssd"`, `"gzip"`}, ArgTypeAny: {"<any value>"}, }
ArgTypeExamples provides example values for each argument type. Used to show contextual help when user is typing function arguments.
var FunctionSpecs = map[string]FunctionSpec{ "avg": { Name: "avg", Params: []ParamSpec{ {Name: "values", Type: ArgTypeNumber, Variadic: true, Examples: []string{"1, 2, 3", "10, 20", "x, y, z"}}, }, }, "sqrt": { Name: "sqrt", Params: []ParamSpec{ {Name: "value", Type: ArgTypeNumber, Examples: []string{"16", "2", "100"}}, }, }, "accumulate": { Name: "accumulate", Params: []ParamSpec{ {Name: "rate", Type: ArgTypeRate, Examples: []string{"10 MB/s", "100 requests/second", "5 GB/day"}}, {Name: "duration", Type: ArgTypeDuration, Examples: []string{"1 hour", "30 minutes", "1 day"}}, }, }, "convert_rate": { Name: "convert_rate", Params: []ParamSpec{ {Name: "rate", Type: ArgTypeRate, Examples: []string{"10 MB/s", "1 GB/hour", "100 req/s"}}, {Name: "time_unit", Type: ArgTypeString, Examples: []string{`"per second"`, `"per hour"`, `"per day"`}}, }, }, "downtime": { Name: "downtime", Params: []ParamSpec{ {Name: "availability", Type: ArgTypePercentage, Examples: []string{"99.9%", "99.99%", "99%"}}, {Name: "duration", Type: ArgTypeDuration, Examples: []string{"1 year", "1 month", "1 day"}}, }, }, "rtt": { Name: "rtt", Params: []ParamSpec{ {Name: "scope", Type: ArgTypeString, Examples: quotedNames(identifiers.NetworkScopes)}, }, }, "throughput": { Name: "throughput", Params: []ParamSpec{ {Name: "network_type", Type: ArgTypeString, Examples: quotedNames(identifiers.NetworkTypes)}, }, }, "transfer_time": { Name: "transfer_time", Params: []ParamSpec{ {Name: "size", Type: ArgTypeQuantity, Examples: []string{"1 GB", "100 MB", "10 TB"}}, {Name: "scope", Type: ArgTypeString, Examples: quotedNames(identifiers.NetworkScopes)}, {Name: "network_type", Type: ArgTypeString, Examples: quotedNames(identifiers.NetworkTypes)}, }, }, "read": { Name: "read", Params: []ParamSpec{ {Name: "size", Type: ArgTypeQuantity, Examples: []string{"1 MB", "4 KB", "1 GB"}}, {Name: "storage_type", Type: ArgTypeString, Examples: quotedNames(identifiers.StorageTypes)}, }, }, "seek": { Name: "seek", Params: []ParamSpec{ {Name: "storage_type", Type: ArgTypeString, Examples: quotedNames(identifiers.StorageTypes)}, }, }, "compress": { Name: "compress", Params: []ParamSpec{ {Name: "size", Type: ArgTypeQuantity, Examples: []string{"1 MB", "100 KB", "1 GB"}}, {Name: "compression_type", Type: ArgTypeString, Examples: quotedNames(identifiers.CompressionTypes)}, }, }, "compound": { Name: "compound", Params: []ParamSpec{ {Name: "principal", Type: ArgTypeAny, Examples: []string{"1000", "$1000", "100 users"}}, {Name: "rate", Type: ArgTypePercentage, Examples: []string{"5%", "0.05", "12%"}}, {Name: "periods", Type: ArgTypeNumber, Examples: []string{"10", "12", "5"}}, {Name: "period", Type: ArgTypeString, Optional: true, Examples: []string{"month", "compounded:monthly", "compounded:quarterly"}}, }, }, "grow": { Name: "grow", Params: []ParamSpec{ {Name: "amount", Type: ArgTypeAny, Examples: []string{"100", "50 GB", "1000 users"}}, {Name: "increment", Type: ArgTypeAny, Examples: []string{"20 GB", "100 users", "5"}}, {Name: "periods", Type: ArgTypeNumber, Examples: []string{"12", "6", "24"}}, }, }, "depreciate": { Name: "depreciate", Params: []ParamSpec{ {Name: "value", Type: ArgTypeAny, Examples: []string{"10000", "$50000", "1000 units"}}, {Name: "rate", Type: ArgTypePercentage, Examples: []string{"20%", "0.15", "10%"}}, {Name: "periods", Type: ArgTypeNumber, Examples: []string{"5", "10", "7"}}, {Name: "salvage", Type: ArgTypeAny, Optional: true, Examples: []string{"$5000", "1000"}}, }, }, "capacity": { Name: "capacity", Params: []ParamSpec{ {Name: "demand", Type: ArgTypeRate, Examples: []string{"1000 requests/second", "10 GB/day"}}, {Name: "capacity_per_unit", Type: ArgTypeRate, Examples: []string{"100 requests/second", "1 GB/day"}}, {Name: "unit", Type: ArgTypeString, Examples: []string{`"servers"`, `"instances"`}}, {Name: "buffer", Type: ArgTypePercentage, Optional: true, Examples: []string{"20%", "50%"}}, }, }, }
FunctionSpecs defines the type signatures for all built-in functions. This is the authoritative source for parameter type information.
Functions ¶
func AreUnitsCompatible ¶
AreUnitsCompatible checks if two units are compatible for arithmetic USER REQUIREMENT: Used for "10 meters + 5 kg" incompatibility detection
func GetExamplesForType ¶
GetExamplesForType returns example values for a given argument type.
func IsDataSizeUnit ¶
IsDataSizeUnit checks if a unit is a data size unit (bytes or bits).
func NormalizeCurrencySymbol ¶
NormalizeCurrencySymbol converts a currency symbol to its ISO code. Returns the normalized code and true if it's a known symbol, or the original value and false otherwise.
func ValidateCurrencyCode ¶
ValidateCurrencyCode checks if a currency code is valid (ISO 4217 or common symbol)
Types ¶
type ArgType ¶
type ArgType string
ArgType represents the semantic type expected for a function argument. This defines what kinds of values are valid for each parameter position.
const ( ArgTypeNumber ArgType = "number" // Plain number: 42, 3.14, 1e6 ArgTypeQuantity ArgType = "quantity" // Number with unit: 10 meters, 5 kg, 100 MB ArgTypeRate ArgType = "rate" // Quantity per time: 10 MB/s, 60 km/h ArgTypeDuration ArgType = "duration" // Time span: 2 hours, 30 minutes ArgTypePercentage ArgType = "percentage" // Percentage: 99.9%, 15% ArgTypeString ArgType = "string" // String literal: "datacenter", "ssd" ArgTypeAny ArgType = "any" // Any value type )
type Checker ¶
type Checker struct {
// contains filtered or unexported fields
}
Checker performs semantic validation on AST nodes.
func NewChecker ¶
func NewChecker() *Checker
NewChecker creates a new semantic checker with an empty environment.
func NewCheckerWithEnv ¶
func NewCheckerWithEnv(env *Environment) *Checker
NewCheckerWithEnv creates a new checker with a pre-populated environment. Useful for continuing validation with existing variable bindings.
func (*Checker) Check ¶
func (c *Checker) Check(nodes []ast.Node) []Diagnostic
Check validates a list of AST nodes and returns all diagnostics found. This is the main entry point for semantic validation.
func (*Checker) GetEnvironment ¶
func (c *Checker) GetEnvironment() *Environment
GetEnvironment returns the current environment (for testing/debugging).
func (*Checker) SetFrontmatter ¶ added in v1.6.2
func (c *Checker) SetFrontmatter(fm FrontmatterInfo)
SetFrontmatter provides frontmatter context for @directive validation. Without frontmatter, @directive references produce "missing frontmatter" errors.
type DataSizeBase ¶
type DataSizeBase int
DataSizeBase represents the numeric base of a data size unit.
const ( // DataSizeBaseNone indicates the unit is not a data size unit. DataSizeBaseNone DataSizeBase = iota // DataSizeBaseBinary indicates 1024-based units (KiB, MiB, GiB, etc.) DataSizeBaseBinary // DataSizeBaseDecimal indicates 1000-based units (kbps, Mbps, Gbps, Kbit, etc.) DataSizeBaseDecimal )
func GetDataSizeBase ¶
func GetDataSizeBase(unit string) DataSizeBase
GetDataSizeBase returns the numeric base for a data size unit. Returns DataSizeBaseNone if the unit is not a recognized data size unit.
type Diagnostic ¶
type Diagnostic struct {
Severity Severity
Code string // Diagnostic code: "invalid_currency_code", "type_mismatch", etc.
Message string // Short, human-readable error message (e.g., "unknown currency")
Detailed string // Detailed explanation with context and guidance
Link string // Optional documentation link for more information
Range *ast.Range // Location in source code
}
Diagnostic represents a semantic validation issue. USER REQUIREMENT: Both short and detailed messages for better UX
func CheckTypeCompatibility ¶
func CheckTypeCompatibility(left, right TypeInfo, operator string, r *ast.Range) *Diagnostic
CheckTypeCompatibility validates that an operation is type-compatible. Returns an error diagnostic if incompatible, nil if compatible.
func CreateIncompatibleCurrenciesDiagnostic ¶
func CreateIncompatibleCurrenciesDiagnostic(code1, code2 string, operation string, node ast.Node) *Diagnostic
CreateIncompatibleCurrenciesDiagnostic creates an ERROR diagnostic for incompatible currency operations.
func CreateInvalidCurrencyDiagnostic ¶
func CreateInvalidCurrencyDiagnostic(code string, node ast.Node) *Diagnostic
CreateInvalidCurrencyDiagnostic creates a HINT diagnostic for an invalid currency code. This suggests that the identifier might have been intended as a unit instead.
func ValidateCurrencyCodeWithDiagnostic ¶
func ValidateCurrencyCodeWithDiagnostic(code string) (valid bool, diag *Diagnostic)
ValidateCurrencyCodeWithDiagnostic validates a currency code and returns enhanced diagnostic USER REQUIREMENT: Provide both short and detailed messages with documentation link
type Environment ¶
type Environment struct {
// contains filtered or unexported fields
}
Environment tracks variable bindings during semantic analysis. This is separate from Go's context.Context - it's simply variable storage.
func NewEnvironment ¶
func NewEnvironment() *Environment
NewEnvironment creates a new empty environment.
func (*Environment) Clone ¶
func (e *Environment) Clone() *Environment
Clone creates a shallow copy of the environment. Useful for creating scoped environments.
func (*Environment) Get ¶
func (e *Environment) Get(name string) (types.Type, bool)
Get retrieves a variable binding. Returns the value and true if found, nil and false if not found.
func (*Environment) GetAllVariables ¶
func (e *Environment) GetAllVariables() map[string]types.Type
GetAllVariables returns the map of all variables.
func (*Environment) GetInfo ¶
func (e *Environment) GetInfo(name string) (*VarInfo, bool)
GetInfo retrieves full variable information including range.
func (*Environment) Has ¶
func (e *Environment) Has(name string) bool
Has checks if a variable is defined.
func (*Environment) Set ¶
func (e *Environment) Set(name string, value types.Type)
Set stores a variable binding with optional range information.
func (*Environment) SetWithRange ¶
SetWithRange stores a variable binding with range information.
type FrontmatterInfo ¶ added in v1.6.2
type FrontmatterInfo interface {
HasScale() bool
HasGlobals() bool
HasGlobal(name string) bool
GlobalKeys() []string
}
FrontmatterInfo provides the frontmatter context needed by the semantic checker to validate @directive references. This interface breaks the import cycle between spec/semantic and spec/document.
type FunctionSpec ¶
type FunctionSpec struct {
Name string // Function name
Params []ParamSpec // Parameter specifications
}
FunctionSpec describes a function's type signature for semantic analysis. This is separate from the implementation - it defines what's valid.
func GetFunctionSpec ¶
func GetFunctionSpec(name string) *FunctionSpec
GetFunctionSpec returns the type specification for a function by name. Returns nil if the function is not found.
func (*FunctionSpec) GetParamAtIndex ¶
func (f *FunctionSpec) GetParamAtIndex(index int) *ParamSpec
GetParamAtIndex returns the parameter spec for the given argument position. Handles variadic parameters (last param applies to all subsequent args).
type ParamSpec ¶
type ParamSpec struct {
Name string // Parameter name (e.g., "rate", "duration")
Type ArgType // Expected argument type
Optional bool // True if parameter has a default value
Variadic bool // True if parameter accepts multiple values
Examples []string // Example values showing how to create this type
}
ParamSpec describes a function parameter with type constraints and examples. This is the semantic definition - what types are valid for this parameter.
type QuantityType ¶
type QuantityType string
QuantityType represents the type of a physical quantity
const ( QuantityLength QuantityType = "Length" QuantityMass QuantityType = "Mass" QuantityTime QuantityType = "Time" QuantityVolume QuantityType = "Volume" QuantityTemperature QuantityType = "Temperature" QuantitySpeed QuantityType = "Speed" QuantityEnergy QuantityType = "Energy" QuantityPower QuantityType = "Power" QuantityUnknown QuantityType = "Unknown" )
func GetQuantityType ¶
func GetQuantityType(unit string) QuantityType
GetQuantityType returns the quantity type for a given unit