semantic

package
v1.14.0 Latest Latest
Warning

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

Go to latest
Published: Apr 27, 2026 License: MIT Imports: 13 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"

	// Frontmatter diagnostics
	DiagFrontmatterValidation = "frontmatter_validation"

	// Parse diagnostics
	DiagParseError = "parse_error"
)

DiagnosticCode constants for all diagnostic types

Variables

This section is empty.

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 HintForDiagnostic added in v1.8.0

func HintForDiagnostic(code, message string) string

HintForDiagnostic returns a user-facing hint for a diagnostic based on its code and message. This is the canonical source of hint definitions — the TUI layer delegates here rather than defining hints itself.

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

func (*Checker) SetLineOffset added in v1.8.0

func (c *Checker) SetLineOffset(offset int)

SetLineOffset sets the number of lines preceding this calc block in the document (e.g. frontmatter lines). Line numbers embedded in diagnostic messages are adjusted by this offset so they are document-absolute rather than block-relative.

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 CheckFrontmatter added in v1.13.0

func CheckFrontmatter(fm document.Frontmatter) []Diagnostic

CheckFrontmatter validates the populated CalcMark fields of a Frontmatter and returns diagnostics for malformed values. It is a separate exported function (not a method on Checker) because frontmatter validation is purely structural — it has no dependency on accumulated Checker state and can be called independently by tooling (LSP, calcmark-web) that wants to surface frontmatter problems without setting up a full Checker.

Scope: only registered keys (see document.Registry) are validated. Entries in fm.Extra are passthrough by design — they carry no CalcMark semantics — and produce zero diagnostics here.

What this DOES NOT duplicate from the parser:

  • YAML shape errors (e.g., "globals: 42") — the parser already returns a fatal error and the resulting Frontmatter is never constructed.
  • convert_to/scale/measurement sub-key validation — the parser's validateConvertToConfig, validateScaleConfig, and parseMeasurementConfig already enforce those at parse time.

What this DOES catch:

  • Programmatically constructed Frontmatter values that bypass the parser (e.g., a test or a future library caller that builds a Frontmatter by hand and sets ConvertTo.System = "xyz").
  • Cases the parser is too permissive about reaching the typed value, such as exchange rates that became zero or negative after construction.

Diagnostic anchors come from fm.KeyRanges (Unit 3). When a registered key is absent from KeyRanges (e.g., the Frontmatter was built programmatically without source positions), the diagnostic still emits but with a zero-value ast.Range as a fallback so callers can rely on Range != nil.

Complexity: O(|registered keys present in fm|). Per D9/D10, the registry is small and walked linearly; no auxiliary indices are built.

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 environment pre-populated with the built-in mathematical constants (PI, E) so the semantic checker doesn't flag references to them as "undefined variable." The runtime interpreter environment does the equivalent via addConstants — this function keeps the two layers aligned.

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 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
	TypePercentage
)

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