semantic

package
v1.6.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 8, 2026 License: MIT Imports: 11 Imported by: 0

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:

  1. Environment Setup: Tracks variable definitions and their types
  2. AST Traversal: Visits each node and validates semantics
  3. 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

View Source
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

View Source
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.

View Source
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

func AreUnitsCompatible(unit1, unit2 string) bool

AreUnitsCompatible checks if two units are compatible for arithmetic USER REQUIREMENT: Used for "10 meters + 5 kg" incompatibility detection

func GetExamplesForType

func GetExamplesForType(argType ArgType) []string

GetExamplesForType returns example values for a given argument type.

func IsDataSizeUnit

func IsDataSizeUnit(unit string) bool

IsDataSizeUnit checks if a unit is a data size unit (bytes or bits).

func NormalizeCurrencySymbol

func NormalizeCurrencySymbol(symbolOrCode string) (string, bool)

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

func ValidateCurrencyCode(code string) bool

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

func (e *Environment) SetWithRange(name string, value types.Type, r *ast.Range)

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

type Severity

type Severity int

Severity represents the severity level of a diagnostic.

const (
	// Error indicates a critical error that prevents execution.
	Error Severity = iota
	// Warning indicates a semantic issue that should be addressed.
	Warning
	// Hint indicates a suggestion or style recommendation.
	Hint
)

func (Severity) String

func (s Severity) String() string

String returns the string representation of the severity.

type TypeInfo

type TypeInfo struct {
	Type types.Type
	Kind TypeKind
}

TypeInfo represents type information for a node.

type TypeKind

type TypeKind int

TypeKind represents the kind of type.

const (
	TypeNumber TypeKind = iota
	TypeCurrency
	TypeBoolean
	TypeDate
	TypeTime
	TypeDuration
	TypeQuantity
)

type VarInfo

type VarInfo struct {
	Type  types.Type
	Range *ast.Range
}

VarInfo tracks information about a variable definition

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL