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
- func AreUnitsCompatible(unit1, unit2 string) bool
- func HintForDiagnostic(code, message string) string
- func IsDataSizeUnit(unit string) bool
- func NormalizeCurrencySymbol(symbolOrCode string) (string, bool)
- func ValidateCurrencyCode(code string) bool
- type Checker
- type DataSizeBase
- type Diagnostic
- func CheckFrontmatter(fm document.Frontmatter) []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 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" // Frontmatter diagnostics DiagFrontmatterValidation = "frontmatter_validation" // Parse diagnostics DiagParseError = "parse_error" )
DiagnosticCode constants for all diagnostic types
Variables ¶
This section is empty.
Functions ¶
func AreUnitsCompatible ¶
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
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 ¶
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 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
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 ¶
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