Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 81 additions & 4 deletions doc/preprocessor-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ The Second Life Script Preprocessor is a comprehensive tool that supports advanc
5. [Include vs Require Behavior](#include-vs-require-behavior)
6. [Macro Definitions (Defines)](#macro-definitions-defines)
7. [Conditional Processing](#conditional-processing)
8. [Complete Examples](#complete-examples)
9. [Best Practices](#best-practices)
10. [Limitations](#limitations)
11. [Integration with VS Code Extension](#integration-with-vs-code-extension)
8. [LSL Preprocessor features](#lsl-preprocessor-features)
9. [Complete Examples](#complete-examples)
10. [Best Practices](#best-practices)
11. [Limitations](#limitations)
12. [Integration with VS Code Extension](#integration-with-vs-code-extension)

## Overview

Expand Down Expand Up @@ -495,6 +496,53 @@ integer result = SQUARE(ADD(2, 3)); // Expands to ((2 + 3) * (2 + 3))
integer result2 = CUBE(ADD(1, 2)); // Expands to ((1 + 2) * ((1 + 2) * (1 + 2)))
```

### Variadic Macros

Macro function can be variadic, allowing you to pass any number of arguments (usually to handle them as a list)

```lsl
#define LOG(type,...) llOwnerSay(llDumpList2String([type,":",__VA_ARGS__]," "))

// Before preproccessing
LOG("Test",1,2,3,4);

// Result
llOwnerSay(llDumpList2String(["Test",":",1,2,3,4]," "));
```

### Empty Function macros

A common usecase for function macro's is debug statements only when a `DEBUG` flag is set, you can achieve this with a conditional function macro with one instance having an empty body

```lsl
#ifdef DEBUG
#define debug(...) llOwnerSay(llDumpList2String([__VA_ARGS__]," "))
#else
#define debug(...)
#endif

default {
state_entry() {
llOwnerSay("Start");
debug("This is a debug message:", 1, 2, 3);
}
}
```

Would output

```lsl
default {
state_entry() {
llOwnerSay("Start");
;
}
}
```

Which is valid LSL and the extra `;` does not consume bytecode memory.


### Stringization Operator (#)

The stringization operator (`#`) converts macro parameters into string literals. This is particularly useful for debugging, logging, and creating dynamic messages.
Expand Down Expand Up @@ -780,6 +828,35 @@ Includes code if all previous conditions were false:
5. **Comparison Operations**: `==`, `!=`, `>`, `>=`, `<`, `<=`
6. **defined() Function**: `#if defined(MACRO_NAME)`

## LSL Preprocessor features

These are features supported for LSL only, and are mostly to provide parity with common existing tooling.

### Switch statements

If you enable the config `slVscodeEdit.preprocessor.lsl.switchStatements` in the preprocessor section.

The preprocessor can handle switch statements by converting them to if conditionals with jumps.

#### Examples
```lsl
default
{
touch_start(integer num_detected)
{
integer coin = llFloor(llFrand(2.0));
switch(coin) {
case 1: {
llOwnerSay("Heads!");
}
default: {
llOwnerSay("Tails!");
}
}
}
}
```

## Complete Examples

### Feature Toggle System
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default [
require: 'readonly',
module: 'readonly',
exports: 'readonly',
structuredClone: 'readonly',
// Browser/Web globals that Node.js also has
TextEncoder: 'readonly',
// VS Code extension globals
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@
"type": "boolean",
"default": false,
"description": "Enable predefined LSL style preproc macro constants in slua"
},
"slVscodeEdit.preprocessor.lsl.switchStatements": {
"title": "Enable LSL switch statements",
"type": "boolean",
"default": false,
"description": "Enable LSL switch statements in the preprocessor"
}
}
},
Expand Down Expand Up @@ -176,7 +182,7 @@
"lint:fix": "eslint src --fix",
"test": "node ./out/test/runTest.js",
"test-basic": "npx mocha \"./out/test/suite/basic.test.js\" --ui tdd --timeout 5000",
"test-unit": "npx mocha \"./out/test/suite/{basic,preprocessor,include-disk-integration}.test.js\" --ui tdd --timeout 5000",
"test-unit": "npx mocha \"./out/test/suite/{basic,preprocessor,include-disk-integration,lexingpreprocessor}.test.js\" --ui tdd --timeout 5000",
"test-compile": "tsc -p . --outDir out --skipLibCheck",
"precommit": "npm run lint && npm run test-compile && npm run test-unit"
},
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/configinterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export enum ConfigKey {
PreprocessorIncludePaths = 'preprocessor.includePaths',
PreprocessorMaxIncludeDepth = 'preprocessor.maxIncludeDepth',
PreprocessorConstantsInSLua = 'preprocessor.constantsInSLua',
PreprocessorLSLSwitchStatements = 'preprocessor.lsl.switchStatements',
LastSyntaxID = 'syntax.lastID',
AskIfViewerScriptMismatchesMaster = 'sync.askIfViewerScriptMismatchesMaster',
CompareHashBeforeSync = 'sync.compareHashBeforeSync',
Expand Down
18 changes: 14 additions & 4 deletions src/scriptsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { HostInterface, normalizePath } from "./interfaces/hostinterface";
import { SynchService } from "./synchservice";
import { IncludeInfo } from "./shared/parser";
import { sha256 } from "js-sha256";
import { LANGUAGE_CONFIGS } from "./shared/lexer";
import { getLanguageConfig, LanguageLexerConfig } from "./shared/lexer";

//====================================================================
interface TrackedDocument {
Expand Down Expand Up @@ -68,7 +68,7 @@ export class ScriptSync implements vscode.Disposable {

// Create macro processor first
this.language = language;
this.macros = new MacroProcessor(this.language);
this.macros = new MacroProcessor();
this.initializeSystemMacros(language);

this.host = host ?? new VSCodeHost();
Expand Down Expand Up @@ -398,10 +398,12 @@ export class ScriptSync implements vscode.Disposable {
console.log(`Preprocessing enabled for: ${baseName}`);

this.macros.clearNonSystemMacros();
const languageConfig = this.getLanuageConfig();
console.error("LANG CONFIG", languageConfig);
preprocessorResult = await this.preprocessor.process(
originalContent,
normalizePath(masterFilePath),
this.language
languageConfig,
);

if (preprocessorResult.issues && preprocessorResult.issues.length > 0) {
Expand Down Expand Up @@ -439,6 +441,14 @@ export class ScriptSync implements vscode.Disposable {
return finalContent;
}

private getLanuageConfig(): LanguageLexerConfig {
const config = getLanguageConfig(this.language);
if(config.name === "lsl" && this.config.getConfig<boolean>(ConfigKey.PreprocessorLSLSwitchStatements, false)) {
config.directiveKeywords.push("switch");
}
return config;
}

public async handleMasterSaved(): Promise<void> {
try {
// Read the original content
Expand Down Expand Up @@ -494,7 +504,7 @@ export class ScriptSync implements vscode.Disposable {

const path = vscode.workspace.asRelativePath(this.masterDocument.uri.fsPath);

const comment = LANGUAGE_CONFIGS[this.language].lineCommentPrefix;
const comment = this.getLanuageConfig().lineCommentPrefix;
meta.push(`${comment} ================ sl-vscode-plugin meta ================`);
meta.push(`${comment} @file ${path}`);
meta.push(`${comment} @hash ${hash}`);
Expand Down
15 changes: 6 additions & 9 deletions src/shared/conditionalprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
* with proper nesting and state tracking.
*/

import { Token, TokenType, getLanguageConfig, type LanguageLexerConfig } from './lexer';
import { ScriptLanguage } from './languageservice';
import { Token, TokenType, type LanguageLexerConfig } from './lexer';
import type { MacroProcessor } from './macroprocessor';
import { PreprocessorDiagnostic, DiagnosticLocation, ErrorCodes } from './diagnostics';
import { NormalizedPath } from '../interfaces/hostinterface';
Expand Down Expand Up @@ -66,12 +65,10 @@ export interface ConditionalResult {
*/
export class ConditionalProcessor {
private stack: ConditionalBlock[] = [];
private language: ScriptLanguage;
private config: LanguageLexerConfig;
private language: LanguageLexerConfig;

constructor(language: ScriptLanguage) {
constructor(language: LanguageLexerConfig) {
this.language = language;
this.config = getLanguageConfig(language);
}

//#region Public API
Expand Down Expand Up @@ -520,7 +517,7 @@ export class ConditionalProcessor {
private evaluateLogicalOr(tokens: Token[], pos: number): { value: number; pos: number } {
let result = this.evaluateLogicalAnd(tokens, pos);

const orOp = this.config.logicalOperators.or;
const orOp = this.language.logicalOperators.or;

while (result.pos < tokens.length) {
const token = tokens[result.pos];
Expand Down Expand Up @@ -548,7 +545,7 @@ export class ConditionalProcessor {
private evaluateLogicalAnd(tokens: Token[], pos: number): { value: number; pos: number } {
let result = this.evaluateComparison(tokens, pos);

const andOp = this.config.logicalOperators.and;
const andOp = this.language.logicalOperators.and;

while (result.pos < tokens.length) {
const token = tokens[result.pos];
Expand Down Expand Up @@ -692,7 +689,7 @@ export class ConditionalProcessor {
}

const token = tokens[pos];
const notOp = this.config.logicalOperators.not;
const notOp = this.language.logicalOperators.not;

// Handle unary minus
if (token.type === TokenType.OPERATOR && token.value === "-") {
Expand Down
6 changes: 6 additions & 0 deletions src/shared/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export interface DiagnosticLocation {
sourceFile: NormalizedPath;
}

export class DiagnosticError extends Error {
constructor(public diagnostic: PreprocessorDiagnostic) {
super(diagnostic.message);
}
}

//#endregion

//#region Diagnostic Collector
Expand Down
11 changes: 5 additions & 6 deletions src/shared/includeprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
*/

import { NormalizedPath, HostInterface, normalizeJoinPath, normalizePath } from '../interfaces/hostinterface';
import { ScriptLanguage } from './languageservice';
import { Lexer, Token } from './lexer';
import { LanguageLexerConfig, Lexer, Token } from './lexer';
import { MacroProcessor } from './macroprocessor';
import { ConditionalProcessor } from './conditionalprocessor';
import { DiagnosticCollector, DiagnosticSeverity, ErrorCodes } from './diagnostics';
Expand Down Expand Up @@ -63,10 +62,10 @@ export interface IncludeState {
* Processor for handling include directives
*/
export class IncludeProcessor {
private language: ScriptLanguage;
private language: LanguageLexerConfig;
private host: HostInterface;

constructor(language: ScriptLanguage, host: HostInterface) {
constructor(language: LanguageLexerConfig, host: HostInterface) {
this.language = language;
this.host = host;
}
Expand Down Expand Up @@ -124,7 +123,7 @@ export class IncludeProcessor {
}

// Resolve the include file path
const extensions = this.language === "lsl" ? ["lsl"] : ["luau", "lua"];
const extensions = this.language.name === "lsl" ? ["lsl"] : ["luau", "lua"];
let includePaths: string[] = [];
let aliased = false;

Expand Down Expand Up @@ -176,7 +175,7 @@ export class IncludeProcessor {
);
// console.error("Resolve: ", [filename, sourceFile, extensions, includePaths, aliased, allowExternal], resolvedPath);

if(!resolvedPath && this.language == "luau") {
if(!resolvedPath && this.language.name == "luau") {
// Luau require supports default file in folder include mechanic 'init.luau'
if(!filename.toLowerCase().endsWith(".luau") && !filename.toLocaleLowerCase().endsWith(".lua")) {
filename += (filename.length ? path.sep : "") + "init";
Expand Down
Loading
Loading